001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.collections; 018 019import static org.apache.juneau.commons.collections.CacheMode.*; 020import static org.apache.juneau.commons.utils.AssertionUtils.*; 021import static org.apache.juneau.commons.utils.SystemUtils.*; 022import static org.apache.juneau.commons.utils.Utils.*; 023 024import java.util.*; 025import java.util.concurrent.*; 026import java.util.concurrent.atomic.*; 027 028import org.apache.juneau.commons.function.*; 029 030/** 031 * Simple in-memory cache for storing and retrieving objects by two-part keys. 032 * 033 * <h5 class='section'>Overview:</h5> 034 * <p> 035 * This class uses {@link java.util.concurrent.ConcurrentHashMap} internally to provide a thread-safe caching layer with automatic 036 * value computation, cache eviction, and statistics tracking for two-part composite keys. It's designed for 037 * caching expensive-to-compute or frequently-accessed objects indexed by two keys. 038 * 039 * <h5 class='section'>Features:</h5> 040 * <ul class='spaced-list'> 041 * <li>Thread-safe concurrent access without external synchronization 042 * <li>Two-part composite key support 043 * <li>Automatic cache eviction when maximum size is reached 044 * <li>Lazy computation via {@link Function2} supplier pattern 045 * <li>Default supplier support for simplified access 046 * <li>Built-in hit/miss statistics tracking 047 * <li>Optional logging of cache statistics on JVM shutdown 048 * <li>Can be disabled entirely via builder or system property 049 * </ul> 050 * 051 * <h5 class='section'>Usage:</h5> 052 * <p class='bjava'> 053 * <jc>// Create a cache with default supplier</jc> 054 * Cache2<ClassLoader,Class<?>,ClassMeta> <jv>metaCache</jv> = Cache2 055 * .<jsm>of</jsm>(ClassLoader.<jk>class</jk>, Class.<jk>class</jk>, ClassMeta.<jk>class</jk>) 056 * .maxSize(500) 057 * .supplier((cl, c) -> createClassMeta(cl, c)) 058 * .build(); 059 * 060 * <jc>// Retrieve using default supplier</jc> 061 * ClassMeta <jv>meta1</jv> = <jv>metaCache</jv>.get(<jv>classLoader</jv>, String.<jk>class</jk>); 062 * 063 * <jc>// Or override the supplier</jc> 064 * ClassMeta <jv>meta2</jv> = <jv>metaCache</jv>.get(<jv>classLoader</jv>, Integer.<jk>class</jk>, () -> customMeta); 065 * </p> 066 * 067 * <h5 class='section'>Cache Behavior:</h5> 068 * <ul class='spaced-list'> 069 * <li>When a key pair is requested: 070 * <ul> 071 * <li>If the key exists in the cache, the cached value is returned (cache hit) 072 * <li>If the key doesn't exist, the supplier is invoked to compute the value 073 * <li>The computed value is stored in the cache and returned (cache miss) 074 * </ul> 075 * <li>When the cache exceeds {@link Builder#maxSize(int)}, the entire cache is cleared 076 * <li>If the cache is disabled, the supplier is always invoked without caching 077 * <li>Null keys always bypass the cache and invoke the supplier 078 * </ul> 079 * 080 * <h5 class='section'>Environment Variables:</h5> 081 * <p> 082 * The following system properties can be used to configure default cache behavior: 083 * <ul class='spaced-list'> 084 * <li><c>juneau.cache.mode</c> - Cache mode: NONE/WEAK/FULL (default: FULL, case-insensitive) 085 * <li><c>juneau.cache.maxSize</c> - Maximum cache size before eviction (default: 1000) 086 * <li><c>juneau.cache.logOnExit</c> - Log cache statistics on shutdown (default: <jk>false</jk>) 087 * </ul> 088 * 089 * <h5 class='section'>Thread Safety:</h5> 090 * <p> 091 * This class is thread-safe and can be safely used from multiple threads without external synchronization. 092 * However, note that when the cache is cleared due to exceeding max size, there's a small window where 093 * multiple threads might compute the same value. This is acceptable for most use cases as it only affects 094 * performance, not correctness. 095 * 096 * <h5 class='section'>Performance Considerations:</h5> 097 * <ul class='spaced-list'> 098 * <li>Cache operations are O(1) average time complexity 099 * <li>The {@link #get(Object, Object, java.util.function.Supplier)} method uses 100 * {@link java.util.concurrent.ConcurrentHashMap#putIfAbsent(Object, Object)} 101 * to minimize redundant computation in concurrent scenarios 102 * <li>When max size is exceeded, the entire cache is cleared in a single operation 103 * <li>Statistics tracking uses {@link AtomicInteger} for thread-safe counting without locking 104 * </ul> 105 * 106 * <h5 class='section'>Examples:</h5> 107 * <p class='bjava'> 108 * <jc>// Simple cache with defaults</jc> 109 * Cache2<String,String,Config> <jv>cache</jv> = Cache2.<jsm>of</jsm>(String.<jk>class</jk>, String.<jk>class</jk>, Config.<jk>class</jk>).build(); 110 * 111 * <jc>// Cache with custom configuration</jc> 112 * Cache2<String,Integer,User> <jv>userCache</jv> = Cache2 113 * .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>) 114 * .maxSize(500) 115 * .logOnExit() 116 * .supplier((tenant, id) -> userService.findUser(tenant, id)) 117 * .build(); 118 * 119 * <jc>// Disabled cache for testing</jc> 120 * Cache2<String,String,Object> <jv>disabledCache</jv> = Cache2 121 * .<jsm>of</jsm>(String.<jk>class</jk>, String.<jk>class</jk>, Object.<jk>class</jk>) 122 * .disableCaching() 123 * .build(); 124 * </p> 125 * 126 * <h5 class='section'>See Also:</h5> 127 * <ul> 128 * <li class='jc'>{@link Cache} 129 * <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview > juneau-commons</a> 130 * </ul> 131 * 132 * @param <K1> The first key type. Can be an array type for content-based key matching. 133 * @param <K2> The second key type. Can be an array type for content-based key matching. 134 * @param <V> The value type. 135 */ 136public class Cache2<K1,K2,V> { 137 138 /** 139 * Builder for creating configured {@link Cache2} instances. 140 * 141 * <h5 class='section'>Example:</h5> 142 * <p class='bjava'> 143 * Cache2<String,Integer,User> <jv>cache</jv> = Cache2 144 * .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>) 145 * .maxSize(200) 146 * .logOnExit() 147 * .build(); 148 * </p> 149 * 150 * <h5 class='section'>See Also:</h5> 151 * <ul> 152 * <li class='jm'>{@link Cache2#of(Class, Class, Class)} 153 * </ul> 154 * 155 * @param <K1> The first key type. 156 * @param <K2> The second key type. 157 * @param <V> The value type. 158 */ 159 public static class Builder<K1,K2,V> { 160 CacheMode cacheMode; 161 int maxSize; 162 String id; 163 boolean logOnExit; 164 boolean threadLocal; 165 Function2<K1,K2,V> supplier; 166 167 Builder() { 168 cacheMode = env("juneau.cache.mode", CacheMode.FULL); 169 maxSize = env("juneau.cache.maxSize", 1000); 170 logOnExit = env("juneau.cache.logOnExit", false); 171 id = "Cache2"; 172 } 173 174 /** 175 * Builds a new {@link Cache2} instance with the configured settings. 176 * 177 * @return A new immutable {@link Cache2} instance. 178 */ 179 public Cache2<K1,K2,V> build() { 180 return new Cache2<>(this); 181 } 182 183 /** 184 * Sets the caching mode for this cache. 185 * 186 * <p> 187 * Available modes: 188 * <ul> 189 * <li>{@link CacheMode#NONE NONE} - No caching (always invoke supplier) 190 * <li>{@link CacheMode#WEAK WEAK} - Weak caching (uses {@link WeakHashMap}) 191 * <li>{@link CacheMode#FULL FULL} - Full caching (uses {@link ConcurrentHashMap}, default) 192 * </ul> 193 * 194 * @param value The caching mode. 195 * @return This object for method chaining. 196 */ 197 public Builder<K1,K2,V> cacheMode(CacheMode value) { 198 cacheMode = value; 199 return this; 200 } 201 202 /** 203 * Conditionally enables logging of cache statistics when the JVM exits. 204 * 205 * <p> 206 * When enabled, the cache will register a shutdown hook that logs the cache name, 207 * total cache hits, and total cache misses (size of cache) to help analyze cache effectiveness. 208 * 209 * @param value Whether to enable logging on exit. 210 * @param idValue The identifier to use in the log message. 211 * @return This object for method chaining. 212 */ 213 public Builder<K1,K2,V> logOnExit(boolean value, String idValue) { 214 id = idValue; 215 logOnExit = value; 216 return this; 217 } 218 219 /** 220 * Enables logging of cache statistics when the JVM exits. 221 * 222 * <p> 223 * When enabled, the cache will register a shutdown hook that logs the cache name, 224 * total cache hits, and total cache misses (size of cache) to help analyze cache effectiveness. 225 * 226 * <p> 227 * Example output: 228 * <p class='bconsole'> 229 * ClassMeta cache: hits=1523, misses: 47 230 * </p> 231 * 232 * <p> 233 * This is useful for: 234 * <ul> 235 * <li>Performance tuning and identifying caching opportunities 236 * <li>Determining optimal max size values 237 * <li>Monitoring cache efficiency in production 238 * </ul> 239 * 240 * @param value The identifier to use in the log message. 241 * @return This object for method chaining. 242 */ 243 public Builder<K1,K2,V> logOnExit(String value) { 244 id = value; 245 logOnExit = true; 246 return this; 247 } 248 249 /** 250 * Specifies the maximum number of entries allowed in this cache. 251 * 252 * <p> 253 * When the cache size exceeds this value, the <em>entire</em> cache is cleared to make room for new entries. 254 * This is a simple eviction strategy that avoids the overhead of LRU/LFU tracking. 255 * 256 * <p> 257 * Default value: 1000 (or value of system property <c>juneau.cache.maxSize</c>) 258 * 259 * <h5 class='section'>Notes:</h5> 260 * <ul> 261 * <li>Setting this too low may cause excessive cache clearing and reduce effectiveness 262 * <li>Setting this too high may consume excessive memory 263 * <li>For unbounded caching, use {@link Integer#MAX_VALUE} (not recommended for production) 264 * </ul> 265 * 266 * @param value The maximum number of cache entries. Must be positive. 267 * @return This object for method chaining. 268 */ 269 public Builder<K1,K2,V> maxSize(int value) { 270 maxSize = value; 271 return this; 272 } 273 274 /** 275 * Specifies the default supplier function for computing values when keys are not found. 276 * 277 * <p> 278 * This supplier will be used by {@link Cache2#get(Object, Object)} when a key pair is not in the cache. 279 * Individual lookups can override this supplier using 280 * {@link Cache2#get(Object, Object, java.util.function.Supplier)}. 281 * 282 * <h5 class='section'>Example:</h5> 283 * <p class='bjava'> 284 * Cache2<String,Integer,User> <jv>cache</jv> = Cache2 285 * .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>) 286 * .supplier((tenant, id) -> userService.findUser(tenant, id)) 287 * .build(); 288 * 289 * <jc>// Uses default supplier</jc> 290 * User <jv>u</jv> = <jv>cache</jv>.get(<js>"tenant1"</js>, 123); 291 * </p> 292 * 293 * @param value The default supplier function. Can be <jk>null</jk>. 294 * @return This object for method chaining. 295 */ 296 public Builder<K1,K2,V> supplier(Function2<K1,K2,V> value) { 297 supplier = value; 298 return this; 299 } 300 301 /** 302 * Enables thread-local caching. 303 * 304 * <p> 305 * When enabled, each thread gets its own separate cache instance. This is useful for 306 * thread-unsafe objects that need to be cached per thread. 307 * 308 * <p> 309 * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API. 310 * 311 * @return This object for method chaining. 312 * @see Cache.Builder#threadLocal() 313 */ 314 public Builder<K1,K2,V> threadLocal() { 315 threadLocal = true; 316 return this; 317 } 318 319 /** 320 * Sets the caching mode to {@link CacheMode#WEAK WEAK}. 321 * 322 * <p> 323 * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>. 324 * 325 * <p> 326 * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be 327 * garbage collected when keys are no longer strongly referenced elsewhere. 328 * 329 * @return This object for method chaining. 330 * @see #cacheMode(CacheMode) 331 */ 332 public Builder<K1,K2,V> weak() { 333 return cacheMode(WEAK); 334 } 335 336 } 337 338 /** 339 * Creates a new {@link Builder} for constructing a cache with explicit type parameters. 340 * 341 * <p> 342 * This variant allows you to specify the cache's generic types explicitly without passing 343 * the class objects, which is useful when working with complex parameterized types. 344 * 345 * <h5 class='section'>Example:</h5> 346 * <p class='bjava'> 347 * <jc>// Working with complex generic types</jc> 348 * Cache2<Class<?>,Class<? extends Annotation>,List<Annotation>> <jv>cache</jv> = 349 * Cache2.<Class<?>,Class<? extends Annotation>,List<Annotation>><jsm>create</jsm>() 350 * .supplier((k1, k2) -> findAnnotations(k1, k2)) 351 * .build(); 352 * </p> 353 * 354 * @param <K1> The first key type. 355 * @param <K2> The second key type. 356 * @param <V> The value type. 357 * @return A new builder for configuring the cache. 358 */ 359 public static <K1,K2,V> Builder<K1,K2,V> create() { 360 return new Builder<>(); 361 } 362 363 /** 364 * Creates a new {@link Builder} for constructing a cache. 365 * 366 * <h5 class='section'>Example:</h5> 367 * <p class='bjava'> 368 * Cache2<String,Integer,User> <jv>cache</jv> = Cache2 369 * .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>) 370 * .maxSize(100) 371 * .build(); 372 * </p> 373 * 374 * @param <K1> The first key type. 375 * @param <K2> The second key type. 376 * @param <V> The value type. 377 * @param key1 The first key type class (used for type safety). 378 * @param key2 The second key type class (used for type safety). 379 * @param type The value type class. 380 * @return A new builder for configuring the cache. 381 */ 382 public static <K1,K2,V> Builder<K1,K2,V> of(Class<K1> key1, Class<K2> key2, Class<V> type) { 383 return new Builder<>(); 384 } 385 386 // Internal map with Tuple2 keys for content-based equality (especially for arrays) 387 // If threadLocal is true, this is null and threadLocalMap is used instead 388 private final java.util.Map<Tuple2<K1,K2>,V> map; 389 390 private final ThreadLocal<Map<Tuple2<K1,K2>,V>> threadLocalMap; 391 392 private final boolean isThreadLocal; 393 394 private final int maxSize; 395 private final CacheMode cacheMode; 396 private final Function2<K1,K2,V> supplier; 397 private final AtomicInteger cacheHits = new AtomicInteger(); 398 399 /** 400 * Constructor. 401 * 402 * @param builder The builder containing configuration settings. 403 */ 404 protected Cache2(Builder<K1,K2,V> builder) { 405 this.maxSize = builder.maxSize; 406 this.cacheMode = builder.cacheMode; 407 this.supplier = builder.supplier; 408 this.isThreadLocal = builder.threadLocal; 409 410 if (isThreadLocal) { 411 // Thread-local mode: each thread gets its own map 412 if (builder.cacheMode == WEAK) { 413 this.threadLocalMap = ThreadLocal.withInitial(() -> Collections.synchronizedMap(new WeakHashMap<>())); 414 } else { 415 this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>()); 416 } 417 this.map = null; 418 } else { 419 // Normal mode: shared map across all threads 420 if (builder.cacheMode == WEAK) { 421 this.map = Collections.synchronizedMap(new WeakHashMap<>()); 422 } else { 423 this.map = new ConcurrentHashMap<>(); 424 } 425 this.threadLocalMap = null; 426 } 427 if (builder.logOnExit) { 428 shutdownMessage(() -> builder.id + ": hits=" + cacheHits.get() + ", misses: " + size()); 429 } 430 } 431 432 /** 433 * Removes all entries from the cache. 434 */ 435 public void clear() { 436 getMap().clear(); 437 } 438 439 /** 440 * Returns <jk>true</jk> if the cache contains a mapping for the specified key pair. 441 * 442 * @param key1 The first key. Can be <jk>null</jk>. 443 * @param key2 The second key. Can be <jk>null</jk>. 444 * @return <jk>true</jk> if the cache contains the key pair. 445 */ 446 public boolean containsKey(K1 key1, K2 key2) { 447 return getMap().containsKey(Tuple2.of(key1, key2)); 448 } 449 450 /** 451 * Returns <jk>true</jk> if the cache contains one or more entries with the specified value. 452 * 453 * @param value The value to check. 454 * @return <jk>true</jk> if the cache contains the value. 455 */ 456 public boolean containsValue(V value) { 457 // ConcurrentHashMap doesn't allow null values, so null can never be in the cache 458 if (value == null) 459 return false; 460 return getMap().containsValue(value); 461 } 462 463 /** 464 * Retrieves a cached value by key pair using the default supplier. 465 * 466 * <p> 467 * This method uses the default supplier configured via {@link Builder#supplier(Function2)}. 468 * If no default supplier was configured, this method will throw a {@link NullPointerException}. 469 * 470 * <h5 class='section'>Example:</h5> 471 * <p class='bjava'> 472 * Cache2<String,Integer,User> <jv>cache</jv> = Cache2 473 * .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>) 474 * .supplier((tenant, id) -> userService.findUser(tenant, id)) 475 * .build(); 476 * 477 * <jc>// Uses default supplier</jc> 478 * User <jv>u</jv> = <jv>cache</jv>.get(<js>"tenant1"</js>, 123); 479 * </p> 480 * 481 * @param key1 First key component. Can be <jk>null</jk>. 482 * @param key2 Second key component. Can be <jk>null</jk>. 483 * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>. 484 * @throws NullPointerException if no default supplier was configured. 485 */ 486 public V get(K1 key1, K2 key2) { 487 return get(key1, key2, () -> supplier.apply(key1, key2)); 488 } 489 490 /** 491 * Retrieves a cached value by key pair, computing it if necessary using the provided supplier. 492 * 493 * <p> 494 * This method implements the cache-aside pattern: 495 * <ol> 496 * <li>If the key pair exists in the cache, return the cached value (cache hit) 497 * <li>If the key pair doesn't exist, invoke the supplier to compute the value 498 * <li>Store the computed value in the cache using 499 * {@link java.util.concurrent.ConcurrentHashMap#putIfAbsent(Object, Object)} 500 * <li>Return the value 501 * </ol> 502 * 503 * <h5 class='section'>Behavior:</h5> 504 * <ul class='spaced-list'> 505 * <li>If the cache is disabled, always invokes the supplier without caching 506 * <li>If the cache exceeds {@link Builder#maxSize(int)}, clears all entries before storing the new value 507 * <li>Thread-safe: Multiple threads can safely call this method concurrently 508 * <li>The supplier may be called multiple times for the same key pair in concurrent scenarios 509 * (due to {@link java.util.concurrent.ConcurrentHashMap#putIfAbsent(Object, Object)} semantics) 510 * </ul> 511 * 512 * <h5 class='section'>Example:</h5> 513 * <p class='bjava'> 514 * Cache2<String,Integer,User> <jv>cache</jv> = Cache2.<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>).build(); 515 * 516 * <jc>// First call: fetches user and caches it</jc> 517 * User <jv>u1</jv> = <jv>cache</jv>.get(<js>"tenant1"</js>, 123, () -> userService.findUser(<js>"tenant1"</js>, 123)); 518 * 519 * <jc>// Second call: returns cached user instantly</jc> 520 * User <jv>u2</jv> = <jv>cache</jv>.get(<js>"tenant1"</js>, 123, () -> userService.findUser(<js>"tenant1"</js>, 123)); 521 * 522 * <jsm>assert</jsm> <jv>u1</jv> == <jv>u2</jv>; <jc>// Same instance</jc> 523 * </p> 524 * 525 * @param key1 First key component. Can be <jk>null</jk>. 526 * @param key2 Second key component. Can be <jk>null</jk>. 527 * @param supplier The supplier to compute the value if it's not in the cache. Must not be <jk>null</jk>. 528 * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>. 529 */ 530 public V get(K1 key1, K2 key2, java.util.function.Supplier<V> supplier) { 531 assertArgNotNull("supplier", supplier); 532 if (cacheMode == NONE) 533 return supplier.get(); 534 var m = getMap(); 535 var wrapped = Tuple2.of(key1, key2); 536 V v = m.get(wrapped); 537 if (v == null) { 538 if (size() > maxSize) 539 clear(); 540 v = supplier.get(); 541 if (v == null) 542 m.remove(wrapped); 543 else 544 m.putIfAbsent(wrapped, v); 545 } else { 546 cacheHits.incrementAndGet(); 547 } 548 return v; 549 } 550 551 /** 552 * Returns the total number of cache hits since this cache was created. 553 * 554 * <p> 555 * A cache hit occurs when {@link #get(Object, Object)} or 556 * {@link #get(Object, Object, java.util.function.Supplier)} finds an existing cached value 557 * for the requested key pair, avoiding the need to invoke the supplier. 558 * 559 * <h5 class='section'>Cache Effectiveness:</h5> 560 * <p> 561 * You can calculate the cache hit ratio using: 562 * <p class='bjava'> 563 * <jk>int</jk> <jv>hits</jv> = <jv>cache</jv>.getCacheHits(); 564 * <jk>int</jk> <jv>misses</jv> = <jv>cache</jv>.size(); 565 * <jk>int</jk> <jv>total</jv> = <jv>hits</jv> + <jv>misses</jv>; 566 * <jk>double</jk> <jv>hitRatio</jv> = (<jk>double</jk>) <jv>hits</jv> / <jv>total</jv>; <jc>// 0.0 to 1.0</jc> 567 * </p> 568 * 569 * <h5 class='section'>Notes:</h5> 570 * <ul> 571 * <li>This counter is never reset, even when {@link #clear()} is called 572 * <li>Thread-safe using {@link AtomicInteger} 573 * <li>Returns 0 if the cache is disabled 574 * </ul> 575 * 576 * @return The total number of cache hits since creation. 577 */ 578 public int getCacheHits() { return cacheHits.get(); } 579 580 /** 581 * Returns <jk>true</jk> if the cache contains no entries. 582 * 583 * @return <jk>true</jk> if the cache is empty. 584 */ 585 public boolean isEmpty() { return getMap().isEmpty(); } 586 587 /** 588 * Associates the specified value with the specified key pair. 589 * 590 * @param key1 The first key. Can be <jk>null</jk>. 591 * @param key2 The second key. Can be <jk>null</jk>. 592 * @param value The value to associate with the key pair. 593 * @return The previous value associated with the key pair, or <jk>null</jk> if there was no mapping. 594 */ 595 public V put(K1 key1, K2 key2, V value) { 596 var m = getMap(); 597 if (value == null) 598 return m.remove(Tuple2.of(key1, key2)); 599 return m.put(Tuple2.of(key1, key2), value); 600 } 601 602 /** 603 * Removes the entry for the specified key pair from the cache. 604 * 605 * @param key1 The first key. Can be <jk>null</jk>. 606 * @param key2 The second key. Can be <jk>null</jk>. 607 * @return The previous value associated with the key pair, or <jk>null</jk> if there was no mapping. 608 */ 609 public V remove(K1 key1, K2 key2) { 610 return getMap().remove(Tuple2.of(key1, key2)); 611 } 612 613 /** 614 * Returns the number of entries in the cache. 615 * 616 * @return The number of cached entries. 617 */ 618 public int size() { 619 return getMap().size(); 620 } 621 622 /** 623 * Gets the map for the current thread. 624 * 625 * @return The map for the current thread. 626 */ 627 private Map<Tuple2<K1,K2>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; } 628}