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