All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.benmanes.caffeine.cache.Caffeine Maven / Gradle / Ivy

/*
 * Copyright 2014 Ben Manes. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.benmanes.caffeine.cache;

import static java.util.Locale.US;
import static java.util.Objects.requireNonNull;

import java.io.Serializable;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.ConcurrentModificationException;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import com.github.benmanes.caffeine.cache.Async.AsyncExpiry;
import com.github.benmanes.caffeine.cache.Async.AsyncRemovalListener;
import com.github.benmanes.caffeine.cache.Async.AsyncWeigher;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.github.benmanes.caffeine.cache.stats.ConcurrentStatsCounter;
import com.github.benmanes.caffeine.cache.stats.StatsCounter;
import com.google.errorprone.annotations.FormatMethod;

/**
 * A builder of {@link Cache}, {@link LoadingCache}, {@link AsyncCache}, and
 * {@link AsyncLoadingCache} instances having a combination of the following features:
 * 
    *
  • automatic loading of entries into the cache, optionally asynchronously *
  • size-based eviction when a maximum is exceeded based on frequency and recency *
  • time-based expiration of entries, measured since last access or last write *
  • asynchronously refresh when the first stale request for an entry occurs *
  • keys automatically wrapped in {@linkplain WeakReference weak} references *
  • values automatically wrapped in {@linkplain WeakReference weak} or * {@linkplain SoftReference soft} references *
  • writes propagated to an external resource *
  • notification of evicted (or otherwise removed) entries *
  • accumulation of cache access statistics *
*

* These features are all optional; caches can be created using all or none of them. By default * cache instances created by {@code Caffeine} will not perform any type of eviction. *

* Usage example: *

{@code
 *   LoadingCache graphs = Caffeine.newBuilder()
 *       .maximumSize(10_000)
 *       .expireAfterWrite(Duration.ofMinutes(10))
 *       .removalListener((Key key, Graph graph, RemovalCause cause) ->
 *           System.out.printf("Key %s was removed (%s)%n", key, cause))
 *       .build(key -> createExpensiveGraph(key));
 * }
*

* The returned cache is implemented as a hash table with similar performance characteristics to * {@link ConcurrentHashMap}. The {@code asMap} view (and its collection views) have weakly * consistent iterators. This means that they are safe for concurrent use, but if other threads * modify the cache after the iterator is created, it is undefined which of these changes, if any, * are reflected in that iterator. These iterators never throw * {@link ConcurrentModificationException}. *

* Note: by default, the returned cache uses equality comparisons (the * {@link Object#equals equals} method) to determine equality for keys or values. However, if * {@link #weakKeys} was specified, the cache uses identity ({@code ==}) comparisons instead for * keys. Likewise, if {@link #weakValues} or {@link #softValues} was specified, the cache uses * identity comparisons for values. *

* Entries are automatically evicted from the cache when any of * {@linkplain #maximumSize(long) maximumSize}, {@linkplain #maximumWeight(long) maximumWeight}, * {@linkplain #expireAfter(Expiry) expireAfter}, {@linkplain #expireAfterWrite expireAfterWrite}, * {@linkplain #expireAfterAccess expireAfterAccess}, {@linkplain #weakKeys weakKeys}, * {@linkplain #weakValues weakValues}, or {@linkplain #softValues softValues} are requested. *

* If {@linkplain #maximumSize(long) maximumSize} or {@linkplain #maximumWeight(long) maximumWeight} * is requested entries may be evicted on each cache modification. *

* If {@linkplain #expireAfter(Expiry) expireAfter}, * {@linkplain #expireAfterWrite expireAfterWrite}, or * {@linkplain #expireAfterAccess expireAfterAccess} is requested then entries may be evicted on * each cache modification, on occasional cache accesses, or on calls to {@link Cache#cleanUp}. A * {@linkplain #scheduler(Scheduler)} may be specified to provide prompt removal of expired entries * rather than waiting until activity triggers the periodic maintenance. Expired entries may be * counted by {@link Cache#estimatedSize()}, but will never be visible to read or write operations. *

* If {@linkplain #weakKeys weakKeys}, {@linkplain #weakValues weakValues}, or * {@linkplain #softValues softValues} are requested, it is possible for a key or value present in * the cache to be reclaimed by the garbage collector. Entries with reclaimed keys or values may be * removed from the cache on each cache modification, on occasional cache accesses, or on calls to * {@link Cache#cleanUp}; such entries may be counted in {@link Cache#estimatedSize()}, but will * never be visible to read or write operations. *

* Certain cache configurations will result in the accrual of periodic maintenance tasks which * will be performed during write operations, or during occasional read operations in the absence of * writes. The {@link Cache#cleanUp} method of the returned cache will also perform maintenance, but * calling it should not be necessary with a high throughput cache. Only caches built with * {@linkplain #maximumSize maximumSize}, {@linkplain #maximumWeight maximumWeight}, * {@linkplain #expireAfter(Expiry) expireAfter}, {@linkplain #expireAfterWrite expireAfterWrite}, * {@linkplain #expireAfterAccess expireAfterAccess}, {@linkplain #weakKeys weakKeys}, * {@linkplain #weakValues weakValues}, or {@linkplain #softValues softValues} perform periodic * maintenance. *

* The caches produced by {@code Caffeine} are serializable, and the deserialized caches retain all * the configuration properties of the original cache. Note that the serialized form does not * include cache contents, but only configuration. * * @author [email protected] (Ben Manes) * @param the most general key type this builder will be able to create caches for. This is * normally {@code Object} unless it is constrained by using a method like {@code * #removalListener} * @param the most general value type this builder will be able to create caches for. This is * normally {@code Object} unless it is constrained by using a method like {@code * #removalListener} */ public final class Caffeine { static final Logger logger = Logger.getLogger(Caffeine.class.getName()); static final Supplier ENABLED_STATS_COUNTER_SUPPLIER = ConcurrentStatsCounter::new; enum Strength { WEAK, SOFT } static final int UNSET_INT = -1; static final int DEFAULT_INITIAL_CAPACITY = 16; static final int DEFAULT_EXPIRATION_NANOS = 0; static final int DEFAULT_REFRESH_NANOS = 0; boolean strictParsing = true; long maximumSize = UNSET_INT; long maximumWeight = UNSET_INT; int initialCapacity = UNSET_INT; long expireAfterWriteNanos = UNSET_INT; long expireAfterAccessNanos = UNSET_INT; long refreshAfterWriteNanos = UNSET_INT; @Nullable RemovalListener evictionListener; @Nullable RemovalListener removalListener; @Nullable Supplier statsCounterSupplier; @SuppressWarnings("deprecation") @Nullable CacheWriter writer; @Nullable Weigher weigher; @Nullable Expiry expiry; @Nullable Scheduler scheduler; @Nullable Executor executor; @Nullable Ticker ticker; @Nullable Strength keyStrength; @Nullable Strength valueStrength; private Caffeine() {} /** Ensures that the argument expression is true. */ @FormatMethod static void requireArgument(boolean expression, String template, @Nullable Object... args) { if (!expression) { throw new IllegalArgumentException(String.format(template, args)); } } /** Ensures that the argument expression is true. */ static void requireArgument(boolean expression) { if (!expression) { throw new IllegalArgumentException(); } } /** Ensures that the state expression is true. */ static void requireState(boolean expression) { if (!expression) { throw new IllegalStateException(); } } /** Ensures that the state expression is true. */ @FormatMethod static void requireState(boolean expression, String template, @Nullable Object... args) { if (!expression) { throw new IllegalStateException(String.format(template, args)); } } /** Returns the smallest power of two greater than or equal to {@code x}. */ static int ceilingPowerOfTwo(int x) { // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. return 1 << -Integer.numberOfLeadingZeros(x - 1); } /** Returns the smallest power of two greater than or equal to {@code x}. */ static long ceilingPowerOfTwo(long x) { // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. return 1L << -Long.numberOfLeadingZeros(x - 1); } /** * Constructs a new {@code Caffeine} instance with default settings, including strong keys, strong * values, and no automatic eviction of any kind. *

* Note that while this return type is {@code Caffeine}, type parameters on the * {@link #build} methods allow you to create a cache of any key and value type desired. * * @return a new instance with default settings */ @NonNull public static Caffeine newBuilder() { return new Caffeine<>(); } /** * Constructs a new {@code Caffeine} instance with the settings specified in {@code spec}. * * @param spec the specification to build from * @return a new instance with the specification's settings */ @NonNull public static Caffeine from(CaffeineSpec spec) { Caffeine builder = spec.toBuilder(); builder.strictParsing = false; return builder; } /** * Constructs a new {@code Caffeine} instance with the settings specified in {@code spec}. * * @param spec a String in the format specified by {@link CaffeineSpec} * @return a new instance with the specification's settings */ @NonNull public static Caffeine from(String spec) { return from(CaffeineSpec.parse(spec)); } /** * Sets the minimum total size for the internal data structures. Providing a large enough estimate * at construction time avoids the need for expensive resizing operations later, but setting this * value unnecessarily high wastes memory. * * @param initialCapacity minimum total size for the internal data structures * @return this {@code Caffeine} instance (for chaining) * @throws IllegalArgumentException if {@code initialCapacity} is negative * @throws IllegalStateException if an initial capacity was already set */ @NonNull public Caffeine initialCapacity(@NonNegative int initialCapacity) { requireState(this.initialCapacity == UNSET_INT, "initial capacity was already set to %s", this.initialCapacity); requireArgument(initialCapacity >= 0); this.initialCapacity = initialCapacity; return this; } boolean hasInitialCapacity() { return (initialCapacity != UNSET_INT); } int getInitialCapacity() { return hasInitialCapacity() ? initialCapacity : DEFAULT_INITIAL_CAPACITY; } /** * Specifies the executor to use when running asynchronous tasks. The executor is delegated to * when sending removal notifications, when asynchronous computations are performed by * {@link AsyncCache} or {@link LoadingCache#refresh} or {@link #refreshAfterWrite}, or when * performing periodic maintenance. By default, {@link ForkJoinPool#commonPool()} is used. *

* The primary intent of this method is to facilitate testing of caches which have been configured * with {@link #removalListener} or utilize asynchronous computations. A test may instead prefer * to configure the cache to execute tasks directly on the same thread. *

* Beware that configuring a cache with an executor that throws {@link RejectedExecutionException} * may experience non-deterministic behavior. * * @param executor the executor to use for asynchronous execution * @return this {@code Caffeine} instance (for chaining) * @throws NullPointerException if the specified executor is null */ @NonNull public Caffeine executor(@NonNull Executor executor) { requireState(this.executor == null, "executor was already set to %s", this.executor); this.executor = requireNonNull(executor); return this; } @NonNull Executor getExecutor() { return (executor == null) ? ForkJoinPool.commonPool() : executor; } /** * Specifies the scheduler to use when scheduling routine maintenance based on an expiration * event. This augments the periodic maintenance that occurs during normal cache operations to * allow for the prompt removal of expired entries regardless of whether any cache activity is * occurring at that time. By default, {@link Scheduler#disabledScheduler()} is used. *

* The scheduling between expiration events is paced to exploit batching and to minimize * executions in short succession. This minimum difference between the scheduled executions is * implementation-specific, currently at ~1 second (2^30 ns). In addition, the provided scheduler * may not offer real-time guarantees (including {@link ScheduledThreadPoolExecutor}). The * scheduling is best-effort and does not make any hard guarantees of when an expired entry will * be removed. *

* Note for Java 9 and later: consider using {@link Scheduler#systemScheduler()} to * leverage the dedicated, system-wide scheduling thread. * * @param scheduler the scheduler that submits a task to the {@link #executor(Executor)} after a * given delay * @return this {@code Caffeine} instance (for chaining) * @throws NullPointerException if the specified scheduler is null */ @NonNull public Caffeine scheduler(@NonNull Scheduler scheduler) { requireState(this.scheduler == null, "scheduler was already set to %s", this.scheduler); this.scheduler = requireNonNull(scheduler); return this; } @NonNull Scheduler getScheduler() { if ((scheduler == null) || (scheduler == Scheduler.disabledScheduler())) { return Scheduler.disabledScheduler(); } else if (scheduler == Scheduler.systemScheduler()) { return scheduler; } return Scheduler.guardedScheduler(scheduler); } /** * Specifies the maximum number of entries the cache may contain. Note that the cache may evict * an entry before this limit is exceeded or temporarily exceed the threshold while evicting. * As the cache size grows close to the maximum, the cache evicts entries that are less likely to * be used again. For example, the cache may evict an entry because it hasn't been used recently * or very often. *

* When {@code size} is zero, elements will be evicted immediately after being loaded into the * cache. This can be useful in testing, or to disable caching temporarily without a code change. * As eviction is scheduled on the configured {@link #executor}, tests may instead prefer * to configure the cache to execute tasks directly on the same thread. *

* This feature cannot be used in conjunction with {@link #maximumWeight}. * * @param maximumSize the maximum size of the cache * @return this {@code Caffeine} instance (for chaining) * @throws IllegalArgumentException if {@code size} is negative * @throws IllegalStateException if a maximum size or weight was already set */ @NonNull public Caffeine maximumSize(@NonNegative long maximumSize) { requireState(this.maximumSize == UNSET_INT, "maximum size was already set to %s", this.maximumSize); requireState(this.maximumWeight == UNSET_INT, "maximum weight was already set to %s", this.maximumWeight); requireState(this.weigher == null, "maximum size can not be combined with weigher"); requireArgument(maximumSize >= 0, "maximum size must not be negative"); this.maximumSize = maximumSize; return this; } /** * Specifies the maximum weight of entries the cache may contain. Weight is determined using the * {@link Weigher} specified with {@link #weigher}, and use of this method requires a * corresponding call to {@link #weigher} prior to calling {@link #build}. *

* Note that the cache may evict an entry before this limit is exceeded or temporarily exceed * the threshold while evicting. As the cache size grows close to the maximum, the cache * evicts entries that are less likely to be used again. For example, the cache may evict an entry * because it hasn't been used recently or very often. *

* When {@code maximumWeight} is zero, elements will be evicted immediately after being loaded * into the cache. This can be useful in testing, or to disable caching temporarily without a code * change. As eviction is scheduled on the configured {@link #executor}, tests may instead prefer * to configure the cache to execute tasks directly on the same thread. *

* Note that weight is only used to determine whether the cache is over capacity; it has no effect * on selecting which entry should be evicted next. *

* This feature cannot be used in conjunction with {@link #maximumSize}. * * @param maximumWeight the maximum total weight of entries the cache may contain * @return this {@code Caffeine} instance (for chaining) * @throws IllegalArgumentException if {@code maximumWeight} is negative * @throws IllegalStateException if a maximum weight or size was already set */ @NonNull public Caffeine maximumWeight(@NonNegative long maximumWeight) { requireState(this.maximumWeight == UNSET_INT, "maximum weight was already set to %s", this.maximumWeight); requireState(this.maximumSize == UNSET_INT, "maximum size was already set to %s", this.maximumSize); requireArgument(maximumWeight >= 0, "maximum weight must not be negative"); this.maximumWeight = maximumWeight; return this; } /** * Specifies the weigher to use in determining the weight of entries. Entry weight is taken into * consideration by {@link #maximumWeight(long)} when determining which entries to evict, and use * of this method requires a corresponding call to {@link #maximumWeight(long)} prior to calling * {@link #build}. Weights are measured and recorded when entries are inserted into or updated in * the cache, and are thus effectively static during the lifetime of a cache entry. *

* When the weight of an entry is zero it will not be considered for size-based eviction (though * it still may be evicted by other means). *

* Important note: Instead of returning this as a {@code Caffeine} instance, this * method returns {@code Caffeine}. From this point on, either the original reference or * the returned reference may be used to complete configuration and build the cache, but only the * "generic" one is type-safe. That is, it will properly prevent you from building caches whose * key or value types are incompatible with the types accepted by the weigher already provided; * the {@code Caffeine} type cannot do this. For best results, simply use the standard * method-chaining idiom, as illustrated in the documentation at top, configuring a * {@code Caffeine} and building your {@link Cache} all in a single statement. *

* Warning: if you ignore the above advice, and use this {@code Caffeine} to build a cache * whose key or value type is incompatible with the weigher, you will likely experience a * {@link ClassCastException} at some undefined point in the future. * * @param weigher the weigher to use in calculating the weight of cache entries * @param key type of the weigher * @param value type of the weigher * @return the cache builder reference that should be used instead of {@code this} for any * remaining configuration and cache building * @throws IllegalStateException if a weigher was already set */ @NonNull public Caffeine weigher( @NonNull Weigher weigher) { requireNonNull(weigher); requireState(this.weigher == null, "weigher was already set to %s", this.weigher); requireState(!strictParsing || this.maximumSize == UNSET_INT, "weigher can not be combined with maximum size"); @SuppressWarnings("unchecked") Caffeine self = (Caffeine) this; self.weigher = weigher; return self; } boolean evicts() { return getMaximum() != UNSET_INT; } boolean isWeighted() { return (weigher != null); } long getMaximum() { return isWeighted() ? maximumWeight : maximumSize; } @NonNull @SuppressWarnings({"unchecked", "rawtypes"}) Weigher getWeigher(boolean isAsync) { Weigher delegate = (weigher == null) || (weigher == Weigher.singletonWeigher()) ? Weigher.singletonWeigher() : Weigher.boundedWeigher((Weigher) weigher); return isAsync ? (Weigher) new AsyncWeigher(delegate) : delegate; } /** * Specifies that each key (not value) stored in the cache should be wrapped in a * {@link WeakReference} (by default, strong references are used). *

* Warning: when this method is used, the resulting cache will use identity ({@code ==}) * comparison to determine equality of keys. Its {@link Cache#asMap} view will therefore * technically violate the {@link Map} specification (in the same way that {@link IdentityHashMap} * does). *

* Entries with keys that have been garbage collected may be counted in * {@link Cache#estimatedSize()}, but will never be visible to read or write operations; such * entries are cleaned up as part of the routine maintenance described in the class javadoc. *

* This feature cannot be used in conjunction with {@link #writer}. *

* This feature cannot be used in conjunction with {@link #writer} or when {@link #weakKeys()} is * combined with {@link #buildAsync}. * * @return this {@code Caffeine} instance (for chaining) * @throws IllegalStateException if the key strength was already set or the writer was set */ @NonNull public Caffeine weakKeys() { requireState(keyStrength == null, "Key strength was already set to %s", keyStrength); requireState(writer == null, "Weak keys may not be used with CacheWriter"); keyStrength = Strength.WEAK; return this; } boolean isStrongKeys() { return (keyStrength == null); } /** * Specifies that each value (not key) stored in the cache should be wrapped in a * {@link WeakReference} (by default, strong references are used). *

* Weak values will be garbage collected once they are weakly reachable. This makes them a poor * candidate for caching; consider {@link #softValues} instead. *

* Note: when this method is used, the resulting cache will use identity ({@code ==}) * comparison to determine equality of values. *

* Entries with values that have been garbage collected may be counted in * {@link Cache#estimatedSize()}, but will never be visible to read or write operations; such * entries are cleaned up as part of the routine maintenance described in the class javadoc. *

* This feature cannot be used in conjunction with {@link #buildAsync}. * * @return this {@code Caffeine} instance (for chaining) * @throws IllegalStateException if the value strength was already set */ @NonNull public Caffeine weakValues() { requireState(valueStrength == null, "Value strength was already set to %s", valueStrength); valueStrength = Strength.WEAK; return this; } boolean isStrongValues() { return (valueStrength == null); } boolean isWeakValues() { return (valueStrength == Strength.WEAK); } /** * Specifies that each value (not key) stored in the cache should be wrapped in a * {@link SoftReference} (by default, strong references are used). Softly-referenced objects will * be garbage-collected in a globally least-recently-used manner, in response to memory * demand. *

* Warning: in most circumstances it is better to set a per-cache * {@linkplain #maximumSize(long) maximum size} instead of using soft references. You should only * use this method if you are very familiar with the practical consequences of soft references. *

* Note: when this method is used, the resulting cache will use identity ({@code ==}) * comparison to determine equality of values. *

* Entries with values that have been garbage collected may be counted in * {@link Cache#estimatedSize()}, but will never be visible to read or write operations; such * entries are cleaned up as part of the routine maintenance described in the class javadoc. *

* This feature cannot be used in conjunction with {@link #buildAsync}. * * @return this {@code Caffeine} instance (for chaining) * @throws IllegalStateException if the value strength was already set */ @NonNull public Caffeine softValues() { requireState(valueStrength == null, "Value strength was already set to %s", valueStrength); valueStrength = Strength.SOFT; return this; } /** * Specifies that each entry should be automatically removed from the cache once a fixed duration * has elapsed after the entry's creation, or the most recent replacement of its value. *

* Expired entries may be counted in {@link Cache#estimatedSize()}, but will never be visible to * read or write operations. Expired entries are cleaned up as part of the routine maintenance * described in the class javadoc. A {@link #scheduler(Scheduler)} may be configured for a prompt * removal of expired entries. * * @param duration the length of time after an entry is created that it should be automatically * removed * @return this {@code Caffeine} instance (for chaining) * @throws IllegalArgumentException if {@code duration} is negative * @throws IllegalStateException if the time to live or time to idle was already set * @throws ArithmeticException for durations greater than +/- approximately 292 years */ @NonNull public Caffeine expireAfterWrite(@NonNull Duration duration) { return expireAfterWrite(saturatedToNanos(duration), TimeUnit.NANOSECONDS); } /** * Specifies that each entry should be automatically removed from the cache once a fixed duration * has elapsed after the entry's creation, or the most recent replacement of its value. *

* Expired entries may be counted in {@link Cache#estimatedSize()}, but will never be visible to * read or write operations. Expired entries are cleaned up as part of the routine maintenance * described in the class javadoc. A {@link #scheduler(Scheduler)} may be configured for a prompt * removal of expired entries. *

* If you can represent the duration as a {@link java.time.Duration} (which should be preferred * when feasible), use {@link #expireAfterWrite(Duration)} instead. * * @param duration the length of time after an entry is created that it should be automatically * removed * @param unit the unit that {@code duration} is expressed in * @return this {@code Caffeine} instance (for chaining) * @throws IllegalArgumentException if {@code duration} is negative * @throws IllegalStateException if the time to live or variable expiration was already set */ @NonNull public Caffeine expireAfterWrite(@NonNegative long duration, @NonNull TimeUnit unit) { requireState(expireAfterWriteNanos == UNSET_INT, "expireAfterWrite was already set to %s ns", expireAfterWriteNanos); requireState(expiry == null, "expireAfterWrite may not be used with variable expiration"); requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); this.expireAfterWriteNanos = unit.toNanos(duration); return this; } long getExpiresAfterWriteNanos() { return expiresAfterWrite() ? expireAfterWriteNanos : DEFAULT_EXPIRATION_NANOS; } boolean expiresAfterWrite() { return (expireAfterWriteNanos != UNSET_INT); } /** * Specifies that each entry should be automatically removed from the cache once a fixed duration * has elapsed after the entry's creation, the most recent replacement of its value, or its last * access. Access time is reset by all cache read and write operations (including {@code * Cache.asMap().get(Object)} and {@code Cache.asMap().put(K, V)}), but not by operations on the * collection-views of {@link Cache#asMap}. *

* Expired entries may be counted in {@link Cache#estimatedSize()}, but will never be visible to * read or write operations. Expired entries are cleaned up as part of the routine maintenance * described in the class javadoc. A {@link #scheduler(Scheduler)} may be configured for a prompt * removal of expired entries. * * @param duration the length of time after an entry is last accessed that it should be * automatically removed * @return this {@code Caffeine} instance (for chaining) * @throws IllegalArgumentException if {@code duration} is negative * @throws IllegalStateException if the time to idle or time to live was already set * @throws ArithmeticException for durations greater than +/- approximately 292 years */ @NonNull public Caffeine expireAfterAccess(@NonNull Duration duration) { return expireAfterAccess(saturatedToNanos(duration), TimeUnit.NANOSECONDS); } /** * Specifies that each entry should be automatically removed from the cache once a fixed duration * has elapsed after the entry's creation, the most recent replacement of its value, or its last * read. Access time is reset by all cache read and write operations (including * {@code Cache.asMap().get(Object)} and {@code Cache.asMap().put(K, V)}), but not by operations * on the collection-views of {@link Cache#asMap}. *

* Expired entries may be counted in {@link Cache#estimatedSize()}, but will never be visible to * read or write operations. Expired entries are cleaned up as part of the routine maintenance * described in the class javadoc. A {@link #scheduler(Scheduler)} may be configured for a prompt * removal of expired entries. *

* If you can represent the duration as a {@link java.time.Duration} (which should be preferred * when feasible), use {@link #expireAfterAccess(Duration)} instead. * * @param duration the length of time after an entry is last accessed that it should be * automatically removed * @param unit the unit that {@code duration} is expressed in * @return this {@code Caffeine} instance (for chaining) * @throws IllegalArgumentException if {@code duration} is negative * @throws IllegalStateException if the time to idle or variable expiration was already set */ @NonNull public Caffeine expireAfterAccess(@NonNegative long duration, @NonNull TimeUnit unit) { requireState(expireAfterAccessNanos == UNSET_INT, "expireAfterAccess was already set to %s ns", expireAfterAccessNanos); requireState(expiry == null, "expireAfterAccess may not be used with variable expiration"); requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); this.expireAfterAccessNanos = unit.toNanos(duration); return this; } long getExpiresAfterAccessNanos() { return expiresAfterAccess() ? expireAfterAccessNanos : DEFAULT_EXPIRATION_NANOS; } boolean expiresAfterAccess() { return (expireAfterAccessNanos != UNSET_INT); } /** * Specifies that each entry should be automatically removed from the cache once a duration has * elapsed after the entry's creation, the most recent replacement of its value, or its last * read. The expiration time is reset by all cache read and write operations (including * {@code Cache.asMap().get(Object)} and {@code Cache.asMap().put(K, V)}), but not by operations * on the collection-views of {@link Cache#asMap}. *

* Expired entries may be counted in {@link Cache#estimatedSize()}, but will never be visible to * read or write operations. Expired entries are cleaned up as part of the routine maintenance * described in the class javadoc. A {@link #scheduler(Scheduler)} may be configured for a prompt * removal of expired entries. *

* Important note: after invoking this method, do not continue to use this cache * builder reference; instead use the reference this method returns. At runtime, these * point to the same instance, but only the returned reference has the correct generic type * information so as to ensure type safety. For best results, use the standard method-chaining * idiom illustrated in the class documentation above, configuring a builder and building your * cache in a single statement. Failure to heed this advice can result in a * {@link ClassCastException} being thrown by a cache operation at some undefined point in * the future. * * @param expiry the expiry to use in calculating the expiration time of cache entries * @param key type of the weigher * @param value type of the weigher * @return this {@code Caffeine} instance (for chaining) * @throws IllegalStateException if expiration was already set */ @NonNull public Caffeine expireAfter( @NonNull Expiry expiry) { requireNonNull(expiry); requireState(this.expiry == null, "Expiry was already set to %s", this.expiry); requireState(this.expireAfterAccessNanos == UNSET_INT, "Expiry may not be used with expiresAfterAccess"); requireState(this.expireAfterWriteNanos == UNSET_INT, "Expiry may not be used with expiresAfterWrite"); @SuppressWarnings("unchecked") Caffeine self = (Caffeine) this; self.expiry = expiry; return self; } boolean expiresVariable() { return expiry != null; } @SuppressWarnings("unchecked") @Nullable Expiry getExpiry(boolean isAsync) { return isAsync && (expiry != null) ? (Expiry) new AsyncExpiry<>(expiry) : (Expiry) expiry; } /** * Specifies that active entries are eligible for automatic refresh once a fixed duration has * elapsed after the entry's creation, or the most recent replacement of its value. The semantics * of refreshes are specified in {@link LoadingCache#refresh}, and are performed by calling {@link * CacheLoader#reload}. *

* Automatic refreshes are performed when the first stale request for an entry occurs. The request * triggering refresh will make an asynchronous call to {@link CacheLoader#reload} and immediately * return the old value. *

* Note: all exceptions thrown during refresh will be logged and then swallowed. * * @param duration the length of time after an entry is created that it should be considered * stale, and thus eligible for refresh * @return this {@code Caffeine} instance (for chaining) * @throws IllegalArgumentException if {@code duration} is zero or negative * @throws IllegalStateException if the refresh interval was already set * @throws ArithmeticException for durations greater than +/- approximately 292 years */ @NonNull public Caffeine refreshAfterWrite(@NonNull Duration duration) { return refreshAfterWrite(saturatedToNanos(duration), TimeUnit.NANOSECONDS); } /** * Specifies that active entries are eligible for automatic refresh once a fixed duration has * elapsed after the entry's creation, or the most recent replacement of its value. The semantics * of refreshes are specified in {@link LoadingCache#refresh}, and are performed by calling * {@link CacheLoader#reload}. *

* Automatic refreshes are performed when the first stale request for an entry occurs. The request * triggering refresh will make an asynchronous call to {@link CacheLoader#reload} and immediately * return the old value. *

* Note: all exceptions thrown during refresh will be logged and then swallowed. *

* If you can represent the duration as a {@link java.time.Duration} (which should be preferred * when feasible), use {@link #refreshAfterWrite(Duration)} instead. * * @param duration the length of time after an entry is created that it should be considered * stale, and thus eligible for refresh * @param unit the unit that {@code duration} is expressed in * @return this {@code Caffeine} instance (for chaining) * @throws IllegalArgumentException if {@code duration} is zero or negative * @throws IllegalStateException if the refresh interval was already set */ @NonNull public Caffeine refreshAfterWrite(@NonNegative long duration, @NonNull TimeUnit unit) { requireNonNull(unit); requireState(refreshAfterWriteNanos == UNSET_INT, "refreshAfterWriteNanos was already set to %s ns", refreshAfterWriteNanos); requireArgument(duration > 0, "duration must be positive: %s %s", duration, unit); this.refreshAfterWriteNanos = unit.toNanos(duration); return this; } long getRefreshAfterWriteNanos() { return refreshAfterWrite() ? refreshAfterWriteNanos : DEFAULT_REFRESH_NANOS; } boolean refreshAfterWrite() { return refreshAfterWriteNanos != UNSET_INT; } /** * Specifies a nanosecond-precision time source for use in determining when entries should be * expired or refreshed. By default, {@link System#nanoTime} is used. *

* The primary intent of this method is to facilitate testing of caches which have been configured * with {@link #expireAfterWrite}, {@link #expireAfterAccess}, or {@link #refreshAfterWrite}. * * @param ticker a nanosecond-precision time source * @return this {@code Caffeine} instance (for chaining) * @throws IllegalStateException if a ticker was already set * @throws NullPointerException if the specified ticker is null */ @NonNull public Caffeine ticker(@NonNull Ticker ticker) { requireState(this.ticker == null, "Ticker was already set to %s", this.ticker); this.ticker = requireNonNull(ticker); return this; } @NonNull Ticker getTicker() { boolean useTicker = expiresVariable() || expiresAfterAccess() || expiresAfterWrite() || refreshAfterWrite() || isRecordingStats(); return useTicker ? (ticker == null) ? Ticker.systemTicker() : ticker : Ticker.disabledTicker(); } /** * Specifies a listener instance that caches should notify each time an entry is evicted. The * cache will invoke this listener during the atomic operation to remove the entry. In the case of * expiration or reference collection, the entry may be pending removal and will be discarded as * as part of the routine maintenance described in the class documentation above. For a more * prompt notification on expiration a {@link #scheduler(Scheduler)} may be configured. A * {@link #removalListener(RemovalListener)} may be preferred when the listener should be invoked * for any {@linkplain RemovalCause reason}, be performed outside of the atomic operation to * remove the entry, and delegated to the configured {@link #executor(Executor)}. *

* Important note: after invoking this method, do not continue to use this cache * builder reference; instead use the reference this method returns. At runtime, these * point to the same instance, but only the returned reference has the correct generic type * information so as to ensure type safety. For best results, use the standard method-chaining * idiom illustrated in the class documentation above, configuring a builder and building your * cache in a single statement. Failure to heed this advice can result in a * {@link ClassCastException} being thrown by a cache operation at some undefined point in * the future. *

* Warning: any exception thrown by {@code listener} will not be propagated to the * {@code Cache} user, only logged via a {@link Logger}. *

* This feature cannot be used in conjunction when {@link #weakKeys()} is combined with * {@link #buildAsync}. * * @param evictionListener a listener instance that caches should notify each time an entry is * being automatically removed due to eviction * @param the key type of the listener * @param the value type of the listener * @return the cache builder reference that should be used instead of {@code this} for any * remaining configuration and cache building * @throws IllegalStateException if a removal listener was already set * @throws NullPointerException if the specified removal listener is null */ @NonNull public Caffeine evictionListener( @NonNull RemovalListener evictionListener) { requireState(this.evictionListener == null, "eviction listener was already set to %s", this.evictionListener); @SuppressWarnings("unchecked") Caffeine self = (Caffeine) this; self.evictionListener = requireNonNull(evictionListener); return self; } /** * Specifies a listener instance that caches should notify each time an entry is removed for any * {@linkplain RemovalCause reason}. The cache will invoke this listener on the configured * {@link #executor(Executor)} after the entry's removal operation has completed. In the case of * expiration or reference collection, the entry may be pending removal and will be discarded as * as part of the routine maintenance described in the class documentation above. For a more * prompt notification on expiration a {@link #scheduler(Scheduler)} may be configured. An * {@link #evictionListener(RemovalListener)} may be preferred when the listener should be invoked * as part of the atomic operation to remove the entry. *

* Important note: after invoking this method, do not continue to use this cache * builder reference; instead use the reference this method returns. At runtime, these * point to the same instance, but only the returned reference has the correct generic type * information so as to ensure type safety. For best results, use the standard method-chaining * idiom illustrated in the class documentation above, configuring a builder and building your * cache in a single statement. Failure to heed this advice can result in a * {@link ClassCastException} being thrown by a cache operation at some undefined point in * the future. *

* Warning: any exception thrown by {@code listener} will not be propagated to the * {@code Cache} user, only logged via a {@link Logger}. * * @param removalListener a listener instance that caches should notify each time an entry is * removed * @param the key type of the listener * @param the value type of the listener * @return the cache builder reference that should be used instead of {@code this} for any * remaining configuration and cache building * @throws IllegalStateException if a removal listener was already set * @throws NullPointerException if the specified removal listener is null */ @NonNull public Caffeine removalListener( @NonNull RemovalListener removalListener) { requireState(this.removalListener == null, "removal listener was already set to %s", this.removalListener); @SuppressWarnings("unchecked") Caffeine self = (Caffeine) this; self.removalListener = requireNonNull(removalListener); return self; } @SuppressWarnings({"unchecked", "rawtypes"}) @Nullable RemovalListener getRemovalListener(boolean async) { RemovalListener castedListener = (RemovalListener) removalListener; return async && (castedListener != null) ? new AsyncRemovalListener(castedListener, getExecutor()) : castedListener; } /** * Specifies a writer instance that caches should notify each time an entry is explicitly created * or modified, or removed for any {@linkplain RemovalCause reason}. The writer is not notified * when an entry is loaded or computed. Each cache created by this builder will invoke this writer * as part of the atomic operation that modifies the cache. *

* Important note: after invoking this method, do not continue to use this cache * builder reference; instead use the reference this method returns. At runtime, these * point to the same instance, but only the returned reference has the correct generic type * information so as to ensure type safety. For best results, use the standard method-chaining * idiom illustrated in the class documentation above, configuring a builder and building your * cache in a single statement. Failure to heed this advice can result in a * {@link ClassCastException} being thrown by a cache operation at some undefined point in * the future. *

* Warning: any exception thrown by {@code writer} will be propagated to the {@code Cache} * user. *

* This feature cannot be used in conjunction with {@link #weakKeys()}, * {@link #evictionListener(RemovalListener)}, or {@link #buildAsync}. * * @param writer a writer instance that caches should notify each time an entry is explicitly * created or modified, or removed for any reason * @param the key type of the writer * @param the value type of the writer * @return the cache builder reference that should be used instead of {@code this} for any * remaining configuration and cache building * @throws IllegalStateException if a writer was already set or if the key strength is weak * @throws NullPointerException if the specified writer is null * @deprecated Scheduled for removal in version 3.0.0. Consider instead using {@link Map} compute * methods for extending manual write and remove operations, and using * {@link #evictionListener(RemovalListener)} for extending removals due to eviction. */ @NonNull @Deprecated public Caffeine writer( @NonNull CacheWriter writer) { requireState(this.writer == null, "Writer was already set to %s", this.writer); requireState(keyStrength == null, "Weak keys may not be used with CacheWriter"); requireState(evictionListener == null, "Eviction listener may not be used with CacheWriter"); @SuppressWarnings("unchecked") Caffeine self = (Caffeine) this; self.writer = requireNonNull(writer); return self; } @SuppressWarnings({"deprecation", "unchecked"}) CacheWriter getCacheWriter(boolean async) { CacheWriter castedWriter; if (evictionListener == null) { castedWriter = (CacheWriter) writer; } else { castedWriter = new CacheWriterAdapter<>(evictionListener, async); } return (castedWriter == null) ? CacheWriter.disabledWriter() : castedWriter; } /** * Enables the accumulation of {@link CacheStats} during the operation of the cache. Without this * {@link Cache#stats} will return zero for all statistics. Note that recording statistics * requires bookkeeping to be performed with each operation, and thus imposes a performance * penalty on cache operation. * * @return this {@code Caffeine} instance (for chaining) */ @NonNull public Caffeine recordStats() { requireState(this.statsCounterSupplier == null, "Statistics recording was already set"); statsCounterSupplier = ENABLED_STATS_COUNTER_SUPPLIER; return this; } /** * Enables the accumulation of {@link CacheStats} during the operation of the cache. Without this * {@link Cache#stats} will return zero for all statistics. Note that recording statistics * requires bookkeeping to be performed with each operation, and thus imposes a performance * penalty on cache operation. Any exception thrown by the supplied {@link StatsCounter} will be * suppressed and logged. * * @param statsCounterSupplier a supplier instance that returns a new {@link StatsCounter} * @return this {@code Caffeine} instance (for chaining) */ @NonNull public Caffeine recordStats( @NonNull Supplier statsCounterSupplier) { requireState(this.statsCounterSupplier == null, "Statistics recording was already set"); requireNonNull(statsCounterSupplier); this.statsCounterSupplier = () -> StatsCounter.guardedStatsCounter(statsCounterSupplier.get()); return this; } boolean isRecordingStats() { return (statsCounterSupplier != null); } @NonNull Supplier getStatsCounterSupplier() { return (statsCounterSupplier == null) ? StatsCounter::disabledStatsCounter : statsCounterSupplier; } boolean isBounded() { return (maximumSize != UNSET_INT) || (maximumWeight != UNSET_INT) || (expireAfterAccessNanos != UNSET_INT) || (expireAfterWriteNanos != UNSET_INT) || (expiry != null) || (keyStrength != null) || (valueStrength != null); } /** * Builds a cache which does not automatically load values when keys are requested unless a * mapping function is provided. Note that multiple threads can concurrently load values for * distinct keys. *

* Consider {@link #build(CacheLoader)} instead, if it is feasible to implement a * {@code CacheLoader}. *

* This method does not alter the state of this {@code Caffeine} instance, so it can be invoked * again to create multiple independent caches. * * @param the key type of the cache * @param the value type of the cache * @return a cache having the requested features */ @NonNull public Cache build() { requireWeightWithWeigher(); requireNonLoadingCache(); @SuppressWarnings("unchecked") Caffeine self = (Caffeine) this; return isBounded() ? new BoundedLocalCache.BoundedLocalManualCache<>(self) : new UnboundedLocalCache.UnboundedLocalManualCache<>(self); } /** * Builds a cache, which either returns an already-loaded value for a given key or atomically * computes or retrieves it using the supplied {@code CacheLoader}. If another thread is currently * loading the value for this key, simply waits for that thread to finish and returns its loaded * value. Note that multiple threads can concurrently load values for distinct keys. *

* This method does not alter the state of this {@code Caffeine} instance, so it can be invoked * again to create multiple independent caches. * * @param loader the cache loader used to obtain new values * @param the key type of the loader * @param the value type of the loader * @return a cache having the requested features */ @NonNull public LoadingCache build( @NonNull CacheLoader loader) { requireWeightWithWeigher(); @SuppressWarnings("unchecked") Caffeine self = (Caffeine) this; return isBounded() || refreshAfterWrite() ? new BoundedLocalCache.BoundedLocalLoadingCache<>(self, loader) : new UnboundedLocalCache.UnboundedLocalLoadingCache<>(self, loader); } /** * Builds a cache which does not automatically load values when keys are requested unless a * mapping function is provided. The returned {@link CompletableFuture} may be already loaded or * currently computing the value for a given key. If the asynchronous computation fails or * computes a {@code null} value then the entry will be automatically removed. Note that multiple * threads can concurrently load values for distinct keys. *

* Consider {@link #buildAsync(CacheLoader)} or {@link #buildAsync(AsyncCacheLoader)} instead, if * it is feasible to implement an {@code CacheLoader} or {@code AsyncCacheLoader}. *

* This method does not alter the state of this {@code Caffeine} instance, so it can be invoked * again to create multiple independent caches. *

* This construction cannot be used with {@link #weakValues()}, {@link #softValues()}, * {@link #writer(CacheWriter)}, or when {@link #weakKeys()} are combined with * {@link #evictionListener(RemovalListener)}. * * @param the key type of the cache * @param the value type of the cache * @return a cache having the requested features */ @NonNull public AsyncCache buildAsync() { requireState(valueStrength == null, "Weak or soft values can not be combined with AsyncCache"); requireState(writer == null, "CacheWriter can not be combined with AsyncCache"); requireState(isStrongKeys() || (evictionListener == null), "Weak keys cannot be combined eviction listener and with AsyncLoadingCache"); requireWeightWithWeigher(); requireNonLoadingCache(); @SuppressWarnings("unchecked") Caffeine self = (Caffeine) this; return isBounded() ? new BoundedLocalCache.BoundedLocalAsyncCache<>(self) : new UnboundedLocalCache.UnboundedLocalAsyncCache<>(self); } /** * Builds a cache, which either returns a {@link CompletableFuture} already loaded or currently * computing the value for a given key, or atomically computes the value asynchronously through a * supplied mapping function or the supplied {@code CacheLoader}. If the asynchronous computation * fails or computes a {@code null} value then the entry will be automatically removed. Note that * multiple threads can concurrently load values for distinct keys. *

* This method does not alter the state of this {@code Caffeine} instance, so it can be invoked * again to create multiple independent caches. *

* This construction cannot be used with {@link #weakValues()}, {@link #softValues()}, * {@link #writer(CacheWriter)}, or when {@link #weakKeys()} are combined with * {@link #evictionListener(RemovalListener)}. * * @param loader the cache loader used to obtain new values * @param the key type of the loader * @param the value type of the loader * @return a cache having the requested features */ @NonNull public AsyncLoadingCache buildAsync( @NonNull CacheLoader loader) { return buildAsync((AsyncCacheLoader) loader); } /** * Builds a cache, which either returns a {@link CompletableFuture} already loaded or currently * computing the value for a given key, or atomically computes the value asynchronously through a * supplied mapping function or the supplied {@code AsyncCacheLoader}. If the asynchronous * computation fails or computes a {@code null} value then the entry will be automatically * removed. Note that multiple threads can concurrently load values for distinct keys. *

* This method does not alter the state of this {@code Caffeine} instance, so it can be invoked * again to create multiple independent caches. *

* This construction cannot be used with {@link #weakValues()}, {@link #softValues()}, * {@link #writer(CacheWriter)}, or when {@link #weakKeys()} are combined with * {@link #evictionListener(RemovalListener)}. * * @param loader the cache loader used to obtain new values * @param the key type of the loader * @param the value type of the loader * @return a cache having the requested features */ @NonNull public AsyncLoadingCache buildAsync( @NonNull AsyncCacheLoader loader) { requireState(isStrongValues(),"Weak or soft values cannot be combined with AsyncLoadingCache"); requireState(writer == null, "CacheWriter cannot be combined with AsyncLoadingCache"); requireState(isStrongKeys() || (evictionListener == null), "Weak keys cannot be combined eviction listener and with AsyncLoadingCache"); requireWeightWithWeigher(); requireNonNull(loader); @SuppressWarnings("unchecked") Caffeine self = (Caffeine) this; return isBounded() || refreshAfterWrite() ? new BoundedLocalCache.BoundedLocalAsyncLoadingCache<>(self, loader) : new UnboundedLocalCache.UnboundedLocalAsyncLoadingCache<>(self, loader); } void requireNonLoadingCache() { requireState(refreshAfterWriteNanos == UNSET_INT, "refreshAfterWrite requires a LoadingCache"); } void requireWeightWithWeigher() { if (weigher == null) { requireState(maximumWeight == UNSET_INT, "maximumWeight requires weigher"); } else if (strictParsing) { requireState(maximumWeight != UNSET_INT, "weigher requires maximumWeight"); } else if (maximumWeight == UNSET_INT) { logger.log(Level.WARNING, "ignoring weigher specified without maximumWeight"); } } /** * Returns the number of nanoseconds of the given duration without throwing or overflowing. *

* Instead of throwing {@link ArithmeticException}, this method silently saturates to either * {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE}. This behavior can be useful when decomposing * a duration in order to call a legacy API which requires a {@code long, TimeUnit} pair. */ private static long saturatedToNanos(Duration duration) { // Using a try/catch seems lazy, but the catch block will rarely get invoked (except for // durations longer than approximately +/- 292 years). try { return duration.toNanos(); } catch (ArithmeticException tooBig) { return duration.isNegative() ? Long.MIN_VALUE : Long.MAX_VALUE; } } /** * Returns a string representation for this Caffeine instance. The exact form of the returned * string is not specified. */ @Override public String toString() { StringBuilder s = new StringBuilder(75); s.append(getClass().getSimpleName()).append('{'); int baseLength = s.length(); if (initialCapacity != UNSET_INT) { s.append("initialCapacity=").append(initialCapacity).append(", "); } if (maximumSize != UNSET_INT) { s.append("maximumSize=").append(maximumSize).append(", "); } if (maximumWeight != UNSET_INT) { s.append("maximumWeight=").append(maximumWeight).append(", "); } if (expireAfterWriteNanos != UNSET_INT) { s.append("expireAfterWrite=").append(expireAfterWriteNanos).append("ns, "); } if (expireAfterAccessNanos != UNSET_INT) { s.append("expireAfterAccess=").append(expireAfterAccessNanos).append("ns, "); } if (expiry != null) { s.append("expiry, "); } if (refreshAfterWriteNanos != UNSET_INT) { s.append("refreshAfterWriteNanos=").append(refreshAfterWriteNanos).append("ns, "); } if (keyStrength != null) { s.append("keyStrength=").append(keyStrength.toString().toLowerCase(US)).append(", "); } if (valueStrength != null) { s.append("valueStrength=").append(valueStrength.toString().toLowerCase(US)).append(", "); } if (evictionListener != null) { s.append("evictionListener, "); } if (removalListener != null) { s.append("removalListener, "); } if (writer != null) { s.append("writer, "); } if (s.length() > baseLength) { s.deleteCharAt(s.length() - 2); } return s.append('}').toString(); } @SuppressWarnings("deprecation") static final class CacheWriterAdapter implements CacheWriter, Serializable { private static final long serialVersionUID = 1; final RemovalListener delegate; final boolean isAsync; CacheWriterAdapter(RemovalListener delegate, boolean isAsync) { this.delegate = delegate; this.isAsync = isAsync; } @Override public void write(K key, V value) {} @Override public void delete(K key, @Nullable V value, RemovalCause cause) { if (cause.wasEvicted()) { try { if (isAsync && (value != null)) { @SuppressWarnings("unchecked") CompletableFuture future = (CompletableFuture) value; value = Async.getIfReady(future); } delegate.onRemoval(key, value, cause); } catch (Throwable t) { logger.log(Level.WARNING, "Exception thrown by eviction listener", t); } } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy