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