/*
* 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.Objects.requireNonNull;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ConcurrentModificationException;
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.TimeUnit;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
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;
/**
* A builder of {@link AsyncLoadingCache}, {@link LoadingCache}, and {@link Cache} 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(10, TimeUnit.MINUTES)
* .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 #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 #expireAfterWrite expireAfterWrite} or
* {@linkplain #expireAfterAccess expireAfterAccess} is requested entries may be evicted on each
* cache modification, on occasional cache accesses, or on calls to {@link Cache#cleanUp}. 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 #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 base key type for all caches created by this builder
* @param the base value type for all caches created by this builder
*/
public final class Caffeine {
static final Logger logger = Logger.getLogger(Caffeine.class.getName());
static final Supplier ENABLED_STATS_COUNTER_SUPPLIER = ConcurrentStatsCounter::new;
enum Strength { STRONG, WEAK, SOFT }
static final int UNSET_INT = -1;
static final int DEFAULT_INITIAL_CAPACITY = 0;
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 refreshNanos = UNSET_INT;
long expireAfterWriteNanos = UNSET_INT;
long expireAfterAccessNanos = UNSET_INT;
RemovalListener super K, ? super V> removalListener;
Supplier statsCounterSupplier;
CacheWriter super K, ? super V> writer;
Weigher super K, ? super V> weigher;
Executor executor;
Ticker ticker;
Strength keyStrength;
Strength valueStrength;
private Caffeine() {}
/** Ensures that the argument expression is true. */
static void requireArgument(boolean expression, String template, 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. */
static void requireState(boolean expression, String template, Object... args) {
if (!expression) {
throw new IllegalStateException(String.format(template, args));
}
}
/**
* Constructs a new {@code Caffeine} instance with default settings, including strong keys, strong
* values, and no automatic eviction of any kind.
*
* @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 builder instance
* @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 AsyncLoadingCache} 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 builder instance
* @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 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.
*
* This feature cannot be used in conjunction with {@link #maximumWeight}.
*
* @param maximumSize the maximum size of the cache
* @return this builder instance
* @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 cache. This can be useful in testing, or to disable caching temporarily without a code
* change.
*
* 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 builder instance
* @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);
this.maximumWeight = maximumWeight;
requireArgument(maximumWeight >= 0, "maximum weight must not be negative");
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 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 IllegalArgumentException if {@code size} is negative
* @throws IllegalStateException if a maximum size was already set
*/
@Nonnull
public Caffeine weigher(
@Nonnull Weigher super K1, ? super V1> 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", this.maximumSize);
@SuppressWarnings("unchecked")
Caffeine self = (Caffeine) this;
self.weigher = weigher;
return self;
}
boolean evicts() {
return getMaximum() != UNSET_INT;
}
boolean isWeighted() {
return (weigher != null);
}
@Nonnegative
long getMaximum() {
return isWeighted() ? maximumWeight : maximumSize;
}
@Nonnull @SuppressWarnings("unchecked")
Weigher getWeigher(boolean isAsync) {
Weigher delegate = isWeighted() && (weigher != Weigher.singletonWeigher())
? Weigher.boundedWeigher((Weigher) weigher)
: Weigher.singletonWeigher();
return (Weigher) (isAsync ? 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.
*
* 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}.
*
* @return this builder instance
* @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);
}
boolean isWeakKeys() {
return (keyStrength == Strength.WEAK);
}
/**
* 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 builder instance
* @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);
}
boolean isSoftValues() {
return (valueStrength == Strength.SOFT);
}
/**
* 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 well 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 builder instance
* @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.
*
* @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 builder instance
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to live or time to idle 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);
requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
this.expireAfterWriteNanos = unit.toNanos(duration);
return this;
}
@Nonnegative
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
* 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.
*
* @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 builder instance
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to idle or time to live 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);
requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
this.expireAfterAccessNanos = unit.toNanos(duration);
return this;
}
@Nonnegative
long getExpiresAfterAccessNanos() {
return expiresAfterAccess() ? expireAfterAccessNanos : DEFAULT_EXPIRATION_NANOS;
}
boolean expiresAfterAccess() {
return (expireAfterAccessNanos != UNSET_INT);
}
/**
* 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
* @param unit the unit that {@code duration} is expressed in
* @return this builder instance
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the refresh interval was already set
*/
@Nonnull
public Caffeine refreshAfterWrite(@Nonnegative long duration, @Nonnull TimeUnit unit) {
requireNonNull(unit);
requireState(refreshNanos == UNSET_INT, "refresh was already set to %s ns", refreshNanos);
requireArgument(duration > 0, "duration must be positive: %s %s", duration, unit);
this.refreshNanos = unit.toNanos(duration);
return this;
}
@Nonnegative
long getRefreshAfterWriteNanos() {
return refreshes() ? refreshNanos : DEFAULT_REFRESH_NANOS;
}
boolean refreshes() {
return refreshNanos != 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 builder instance
* @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() {
return expiresAfterAccess() || expiresAfterWrite() || refreshes() || isRecordingStats()
? (ticker == null) ? Ticker.systemTicker() : ticker
: Ticker.disabledTicker();
}
/**
* Specifies a listener instance that caches should notify each time an entry is removed for any
* {@linkplain RemovalCause reason}. Each cache created by this builder will invoke this listener
* as part of the routine maintenance described in the class documentation above.
*
* Warning: 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 super K1, ? super V1> removalListener) {
requireState(this.removalListener == null);
@SuppressWarnings("unchecked")
Caffeine self = (Caffeine) this;
self.removalListener = requireNonNull(removalListener);
return self;
}
RemovalListener getRemovalListener(boolean async) {
@SuppressWarnings("unchecked")
RemovalListener castedListener = (RemovalListener) removalListener;
if (async && (castedListener != null)) {
@SuppressWarnings("unchecked")
RemovalListener asyncListener = (RemovalListener)
new AsyncRemovalListener(castedListener, getExecutor());
return asyncListener;
}
return 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.
*
* Warning: 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()} 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
*/
@Nonnull
public Caffeine writer(
@Nonnull CacheWriter super K1, ? super V1> writer) {
requireState(this.writer == null, "Writer was already set to %s", this.writer);
requireState(keyStrength == null, "Weak keys may not be used with CacheWriter");
@SuppressWarnings("unchecked")
Caffeine self = (Caffeine) this;
self.writer = requireNonNull(writer);
return self;
}
CacheWriter getCacheWriter() {
@SuppressWarnings("unchecked")
CacheWriter castedWriter = (CacheWriter) writer;
return (writer == 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 builder instance
*/
@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 builder instance
*/
@Nonnull
public Caffeine recordStats(
@Nonnull Supplier extends StatsCounter> 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 extends StatsCounter> getStatsCounterSupplier() {
return (statsCounterSupplier == null)
? StatsCounter::disabledStatsCounter
: statsCounterSupplier;
}
boolean isBounded() {
return (maximumSize != UNSET_INT)
|| (maximumWeight != UNSET_INT)
|| (expireAfterAccessNanos != UNSET_INT)
|| (expireAfterWriteNanos != UNSET_INT)
|| (keyStrength != null)
|| (valueStrength != null);
}
/**
* Builds a cache which does not automatically load values when keys are requested.
*
* 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() || refreshes()
? 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
* @throws NullPointerException if the specified cache loader is null
*/
@Nonnull
public LoadingCache build(
@Nonnull CacheLoader super K1, V1> loader) {
requireWeightWithWeigher();
@SuppressWarnings("unchecked")
Caffeine self = (Caffeine) this;
return isBounded() || refreshes()
? new BoundedLocalCache.BoundedLocalLoadingCache(self, loader)
: new UnboundedLocalCache.UnboundedLocalLoadingCache(self, 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 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.
*
* @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
* @throws IllegalStateException if the value strength is weak or soft
* @throws NullPointerException if the specified cache loader is null
*/
@Nonnull
public AsyncLoadingCache buildAsync(
@Nonnull CacheLoader super K1, V1> loader) {
return buildAsync((AsyncCacheLoader super K1, V1>) 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.
*
* @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
* @throws IllegalStateException if the value strength is weak or soft
* @throws NullPointerException if the specified cache loader is null
*/
@Nonnull
public AsyncLoadingCache buildAsync(
@Nonnull AsyncCacheLoader super K1, V1> loader) {
requireState(valueStrength == null);
requireState(writer == null);
requireWeightWithWeigher();
requireNonNull(loader);
@SuppressWarnings("unchecked")
Caffeine self = (Caffeine) this;
return isBounded() || refreshes()
? new BoundedLocalCache.BoundedLocalAsyncLoadingCache(self, loader)
: new UnboundedLocalCache.UnboundedLocalAsyncLoadingCache(self, loader);
}
void requireNonLoadingCache() {
requireState(refreshNanos == 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 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(64);
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 (refreshNanos != UNSET_INT) {
s.append("refreshNanos=").append(refreshNanos).append("ns, ");
}
if (keyStrength != null) {
s.append("keyStrength=").append(keyStrength.toString().toLowerCase()).append(", ");
}
if (valueStrength != null) {
s.append("valueStrength=").append(valueStrength.toString().toLowerCase()).append(", ");
}
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();
}
}