diff options
Diffstat (limited to 'guava-gwt/src-super/com/google/common/cache/super/com/google/common/cache/CacheBuilder.java')
-rw-r--r-- | guava-gwt/src-super/com/google/common/cache/super/com/google/common/cache/CacheBuilder.java | 741 |
1 files changed, 221 insertions, 520 deletions
diff --git a/guava-gwt/src-super/com/google/common/cache/super/com/google/common/cache/CacheBuilder.java b/guava-gwt/src-super/com/google/common/cache/super/com/google/common/cache/CacheBuilder.java index 8b01a0e..4c30815 100644 --- a/guava-gwt/src-super/com/google/common/cache/super/com/google/common/cache/CacheBuilder.java +++ b/guava-gwt/src-super/com/google/common/cache/super/com/google/common/cache/CacheBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Guava Authors + * Copyright (C) 2011 The Guava Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,251 +16,48 @@ package com.google.common.cache; -import static com.google.common.base.Objects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import com.google.common.annotations.GwtCompatible; -import com.google.common.base.Ascii; -import com.google.common.base.Equivalence; -import com.google.common.base.Objects; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; -import com.google.common.base.Ticker; -import com.google.common.cache.AbstractCache.SimpleStatsCounter; -import com.google.common.cache.AbstractCache.StatsCounter; -import com.google.common.cache.LocalCache.Strength; - +import com.google.common.base.Function; +import com.google.common.cache.CacheLoader.InvalidCacheLoadException; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.ExecutionError; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.gwt.user.client.Timer; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.CheckReturnValue; +import javax.annotation.Nullable; /** - * <p>A builder of {@link LoadingCache} and {@link Cache} instances having any combination of the - * following features: - * - * <ul> - * <li>automatic loading of entries into the cache - * <li>least-recently-used eviction when a maximum size is exceeded - * <li>time-based expiration of entries, measured since last access or last write - * <li>keys automatically wrapped in {@linkplain WeakReference weak} references - * <li>values automatically wrapped in {@linkplain WeakReference weak} or - * {@linkplain SoftReference soft} references - * <li>notification of evicted (or otherwise removed) entries - * <li>accumulation of cache access statistics - * </ul> - * - * These features are all optional; caches can be created using all or none of them. By default - * cache instances created by {@code CacheBuilder} will not perform any type of eviction. - * - * <p>Usage example: <pre> {@code - * - * LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() - * .maximumSize(10000) - * .expireAfterWrite(10, TimeUnit.MINUTES) - * .removalListener(MY_LISTENER) - * .build( - * new CacheLoader<Key, Graph>() { - * public Graph load(Key key) throws AnyException { - * return createExpensiveGraph(key); - * } - * });}</pre> - * - * Or equivalently, <pre> {@code - * - * // In real life this would come from a command-line flag or config file - * String spec = "maximumSize=10000,expireAfterWrite=10m"; - * - * LoadingCache<Key, Graph> graphs = CacheBuilder.from(spec) - * .removalListener(MY_LISTENER) - * .build( - * new CacheLoader<Key, Graph>() { - * public Graph load(Key key) throws AnyException { - * return createExpensiveGraph(key); - * } - * });}</pre> - * - * <p>The returned cache is implemented as a hash table with similar performance characteristics to - * {@link ConcurrentHashMap}. It implements all optional operations of the {@link LoadingCache} and - * {@link Cache} interfaces. The {@code asMap} view (and its collection views) have <i>weakly - * consistent iterators</i>. 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}. - * - * <p><b>Note:</b> 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. + * CacheBuilder emulation. * - * <p>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. - * - * <p>If {@linkplain #maximumSize(long) maximumSize} or - * {@linkplain #maximumWeight(long) maximumWeight} is requested entries may be evicted on each cache - * modification. - * - * <p>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 in {@link Cache#size}, but will never be visible to read or write - * operations. - * - * <p>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#size}, but will never be - * visible to read or write operations. - * - * <p>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 absense 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 #removalListener removalListener}, {@linkplain #expireAfterWrite expireAfterWrite}, - * {@linkplain #expireAfterAccess expireAfterAccess}, {@linkplain #weakKeys weakKeys}, - * {@linkplain #weakValues weakValues}, or {@linkplain #softValues softValues} perform periodic - * maintenance. - * - * <p>The caches produced by {@code CacheBuilder} are serializable, and the deserialized caches - * retain all the configuration properties of the original cache. Note that the serialized form does - * <i>not</i> include cache contents, but only configuration. - * - * <p>See the Guava User Guide article on <a href= - * "http://code.google.com/p/guava-libraries/wiki/CachesExplained">caching</a> for a higher-level - * explanation. - * - * @param <K> the base key type for all caches created by this builder - * @param <V> the base value type for all caches created by this builder * @author Charles Fry - * @author Kevin Bourrillion - * @since 10.0 */ -@GwtCompatible(emulated = true) -public final class CacheBuilder<K, V> { +// TODO(fry): eventually we should emmulate LocalCache instead of CacheBuilder +public class CacheBuilder<K, V> { + private static final int UNSET_INT = -1; private static final int DEFAULT_INITIAL_CAPACITY = 16; - private static final int DEFAULT_CONCURRENCY_LEVEL = 4; private static final int DEFAULT_EXPIRATION_NANOS = 0; - private static final int DEFAULT_REFRESH_NANOS = 0; - - static final Supplier<? extends StatsCounter> NULL_STATS_COUNTER = Suppliers.ofInstance( - new StatsCounter() { - @Override - public void recordHits(int count) {} - @Override - public void recordMisses(int count) {} - - @Override - public void recordLoadSuccess(long loadTime) {} - - @Override - public void recordLoadException(long loadTime) {} - - @Override - public void recordEviction() {} - - @Override - public CacheStats snapshot() { - return EMPTY_STATS; - } - }); - static final CacheStats EMPTY_STATS = new CacheStats(0, 0, 0, 0, 0, 0); + private int initialCapacity = -1; + private int concurrencyLevel = -1; + private long expirationMillis = -1; + private int maximumSize = -1; - static final Supplier<StatsCounter> CACHE_STATS_COUNTER = - new Supplier<StatsCounter>() { - @Override - public StatsCounter get() { - return new SimpleStatsCounter(); - } - }; - - enum NullListener implements RemovalListener<Object, Object> { - INSTANCE; - - @Override - public void onRemoval(RemovalNotification<Object, Object> notification) {} - } - - enum OneWeigher implements Weigher<Object, Object> { - INSTANCE; - - @Override - public int weigh(Object key, Object value) { - return 1; - } - } - - static final Ticker NULL_TICKER = new Ticker() { - @Override - public long read() { - return 0; - } - }; - - private static final Logger logger = Logger.getLogger(CacheBuilder.class.getName()); - - static final int UNSET_INT = -1; - - boolean strictParsing = true; - - int initialCapacity = UNSET_INT; - int concurrencyLevel = UNSET_INT; - long maximumSize = UNSET_INT; - long maximumWeight = UNSET_INT; - Weigher<? super K, ? super V> weigher; - - Strength keyStrength; - Strength valueStrength; - - long expireAfterWriteNanos = UNSET_INT; - long expireAfterAccessNanos = UNSET_INT; - long refreshNanos = UNSET_INT; - - Equivalence<Object> keyEquivalence; - Equivalence<Object> valueEquivalence; - - RemovalListener<? super K, ? super V> removalListener; - Ticker ticker; - - Supplier<? extends StatsCounter> statsCounterSupplier = NULL_STATS_COUNTER; - - // TODO(fry): make constructor private and update tests to use newBuilder CacheBuilder() {} - /** - * Constructs a new {@code CacheBuilder} instance with default settings, including strong keys, - * strong values, and no automatic eviction of any kind. - */ public static CacheBuilder<Object, Object> newBuilder() { return new CacheBuilder<Object, Object>(); } - Equivalence<Object> getKeyEquivalence() { - return firstNonNull(keyEquivalence, getKeyStrength().defaultEquivalence()); - } - - Equivalence<Object> getValueEquivalence() { - return firstNonNull(valueEquivalence, getValueStrength().defaultEquivalence()); - } - - /** - * Sets the minimum total size for the internal hash tables. For example, if the initial capacity - * is {@code 60}, and the concurrency level is {@code 8}, then eight segments are created, each - * having a hash table of size eight. Providing a large enough estimate at construction time - * avoids the need for expensive resizing operations later, but setting this value unnecessarily - * high wastes memory. - * - * @throws IllegalArgumentException if {@code initialCapacity} is negative - * @throws IllegalStateException if an initial capacity was already set - */ public CacheBuilder<K, V> initialCapacity(int initialCapacity) { checkState(this.initialCapacity == UNSET_INT, "initial capacity was already set to %s", this.initialCapacity); @@ -269,354 +66,258 @@ public final class CacheBuilder<K, V> { return this; } - int getInitialCapacity() { + private int getInitialCapacity() { return (initialCapacity == UNSET_INT) ? DEFAULT_INITIAL_CAPACITY : initialCapacity; } - /** - * Guides the allowed concurrency among update operations. Used as a hint for internal sizing. The - * table is internally partitioned to try to permit the indicated number of concurrent updates - * without contention. Because assignment of entries to these partitions is not necessarily - * uniform, the actual concurrency observed may vary. Ideally, you should choose a value to - * accommodate as many threads as will ever concurrently modify the table. Using a significantly - * higher value than you need can waste space and time, and a significantly lower value can lead - * to thread contention. But overestimates and underestimates within an order of magnitude do not - * usually have much noticeable impact. A value of one permits only one thread to modify the cache - * at a time, but since read operations and cache loading computations can proceed concurrently, - * this still yields higher concurrency than full synchronization. - * - * <p> Defaults to 4. <b>Note:</b>The default may change in the future. If you care about this - * value, you should always choose it explicitly. - * - * <p>The current implementation uses the concurrency level to create a fixed number of hashtable - * segments, each governed by its own write lock. The segment lock is taken once for each explicit - * write, and twice for each cache loading computation (once prior to loading the new value, - * and once after loading completes). Much internal cache management is performed at the segment - * granularity. For example, access queues and write queues are kept per segment when they are - * required by the selected eviction algorithm. As such, when writing unit tests it is not - * uncommon to specify {@code concurrencyLevel(1)} in order to achieve more deterministic eviction - * behavior. - * - * <p>Note that future implementations may abandon segment locking in favor of more advanced - * concurrency controls. - * - * @throws IllegalArgumentException if {@code concurrencyLevel} is nonpositive - * @throws IllegalStateException if a concurrency level was already set - */ public CacheBuilder<K, V> concurrencyLevel(int concurrencyLevel) { checkState(this.concurrencyLevel == UNSET_INT, "concurrency level was already set to %s", this.concurrencyLevel); checkArgument(concurrencyLevel > 0); + // GWT technically only supports concurrencyLevel == 1, but we silently + // ignore other positive values. this.concurrencyLevel = concurrencyLevel; return this; } - int getConcurrencyLevel() { - return (concurrencyLevel == UNSET_INT) ? DEFAULT_CONCURRENCY_LEVEL : concurrencyLevel; + public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) { + checkState(expirationMillis == UNSET_INT, "expireAfterWrite was already set to %s ms", + expirationMillis); + checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); + this.expirationMillis = unit.toMillis(duration); + return this; } - /** - * Specifies the maximum number of entries the cache may contain. Note that the cache <b>may evict - * an entry before this limit is exceeded</b>. 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. - * - * <p>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. - * - * <p>This feature cannot be used in conjunction with {@link #maximumWeight}. - * - * @param size the maximum size of the cache - * @throws IllegalArgumentException if {@code size} is negative - * @throws IllegalStateException if a maximum size or weight was already set - */ - public CacheBuilder<K, V> maximumSize(long size) { - checkState(this.maximumSize == UNSET_INT, "maximum size was already set to %s", - this.maximumSize); - checkState(this.maximumWeight == UNSET_INT, "maximum weight was already set to %s", - this.maximumWeight); - checkState(this.weigher == null, "maximum size can not be combined with weigher"); - checkArgument(size >= 0, "maximum size must not be negative"); - this.maximumSize = size; - return this; + private long getExpirationMillis() { + return (expirationMillis == UNSET_INT) ? DEFAULT_EXPIRATION_NANOS : expirationMillis; } - long getMaximumWeight() { - if (expireAfterWriteNanos == 0 || expireAfterAccessNanos == 0) { - return 0; + public CacheBuilder<K, V> maximumSize(int maximumSize) { + if (this.maximumSize != -1) { + throw new IllegalStateException("maximum size of " + maximumSize + " was already set"); + } + if (maximumSize < 0) { + throw new IllegalArgumentException("invalid maximum size: " + maximumSize); } - return (weigher == null) ? maximumSize : maximumWeight; + this.maximumSize = maximumSize; + return this; } - // Make a safe contravariant cast now so we don't have to do it over and over. - @SuppressWarnings("unchecked") - <K1 extends K, V1 extends V> Weigher<K1, V1> getWeigher() { - return (Weigher<K1, V1>) Objects.firstNonNull(weigher, OneWeigher.INSTANCE); + public <K1 extends K, V1 extends V> Cache<K1, V1> build() { + return new LocalManualCache<K1, V1>(this); } - CacheBuilder<K, V> setKeyStrength(Strength strength) { - checkState(keyStrength == null, "Key strength was already set to %s", keyStrength); - keyStrength = checkNotNull(strength); - return this; + public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build( + CacheLoader<? super K1, V1> loader) { + return new LocalLoadingCache<K1, V1>(this, loader); } - Strength getKeyStrength() { - return firstNonNull(keyStrength, Strength.STRONG); - } + private static class LocalManualCache<K, V> + extends AbstractCache<K, V> implements Function<K, V> { + final LocalCache<K, V> localCache; - CacheBuilder<K, V> setValueStrength(Strength strength) { - checkState(valueStrength == null, "Value strength was already set to %s", valueStrength); - valueStrength = checkNotNull(strength); - return this; - } + LocalManualCache(CacheBuilder<? super K, ? super V> builder) { + this(builder, null); + } - Strength getValueStrength() { - return firstNonNull(valueStrength, Strength.STRONG); - } + protected LocalManualCache(CacheBuilder<? super K, ? super V> builder, + CacheLoader<? super K, V> loader) { + this.localCache = new LocalCache<K, V>(builder, loader); + } - /** - * 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. - * - * <p>When {@code duration} is zero, this method hands off to - * {@link #maximumSize(long) maximumSize}{@code (0)}, ignoring any otherwise-specificed maximum - * size or weight. This can be useful in testing, or to disable caching temporarily without a code - * change. - * - * <p>Expired entries may be counted in {@link Cache#size}, 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 - * @throws IllegalArgumentException if {@code duration} is negative - * @throws IllegalStateException if the time to live or time to idle was already set - */ - public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) { - checkState(expireAfterWriteNanos == UNSET_INT, "expireAfterWrite was already set to %s ns", - expireAfterWriteNanos); - checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); - this.expireAfterWriteNanos = unit.toNanos(duration); - return this; - } + // Cache methods - long getExpireAfterWriteNanos() { - return (expireAfterWriteNanos == UNSET_INT) ? DEFAULT_EXPIRATION_NANOS : expireAfterWriteNanos; - } + @Override + public V get(K key) throws ExecutionException { + return localCache.getOrLoad(key); + } - /** - * 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}. - * - * <p>When {@code duration} is zero, this method hands off to - * {@link #maximumSize(long) maximumSize}{@code (0)}, ignoring any otherwise-specificed maximum - * size or weight. This can be useful in testing, or to disable caching temporarily without a code - * change. - * - * <p>Expired entries may be counted in {@link Cache#size}, 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 - * @throws IllegalArgumentException if {@code duration} is negative - * @throws IllegalStateException if the time to idle or time to live was already set - */ - public CacheBuilder<K, V> expireAfterAccess(long duration, TimeUnit unit) { - checkState(expireAfterAccessNanos == UNSET_INT, "expireAfterAccess was already set to %s ns", - expireAfterAccessNanos); - checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); - this.expireAfterAccessNanos = unit.toNanos(duration); - return this; - } + @Override + public V getUnchecked(K key) { + try { + return get(key); + } catch (ExecutionException e) { + throw new UncheckedExecutionException(e.getCause()); + } + } - long getExpireAfterAccessNanos() { - return (expireAfterAccessNanos == UNSET_INT) - ? DEFAULT_EXPIRATION_NANOS : expireAfterAccessNanos; - } + @Override + public final V apply(K key) { + return getUnchecked(key); + } - long getRefreshNanos() { - return (refreshNanos == UNSET_INT) ? DEFAULT_REFRESH_NANOS : refreshNanos; - } + @Override + @Nullable + public V getIfPresent(K key) { + return localCache.get(key); + } - /** - * Specifies a nanosecond-precision time source for use in determining when entries should be - * expired. By default, {@link System#nanoTime} is used. - * - * <p>The primary intent of this method is to facilitate testing of caches which have been - * configured with {@link #expireAfterWrite} or {@link #expireAfterAccess}. - * - * @throws IllegalStateException if a ticker was already set - */ - public CacheBuilder<K, V> ticker(Ticker ticker) { - checkState(this.ticker == null); - this.ticker = checkNotNull(ticker); - return this; - } + @Override + public void put(K key, V value) { + localCache.put(key, value); + } - Ticker getTicker(boolean recordsTime) { - if (ticker != null) { - return ticker; + @Override + public void invalidate(Object key) { + checkNotNull(key); + localCache.remove(key); } - return recordsTime ? Ticker.systemTicker() : NULL_TICKER; - } - /** - * 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. - * - * <p><b>Warning:</b> after invoking this method, do not continue to use <i>this</i> cache - * builder reference; instead use the reference this method <i>returns</i>. 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 <i>undefined</i> point in the - * future. - * - * <p><b>Warning:</b> any exception thrown by {@code listener} will <i>not</i> be propagated to - * the {@code Cache} user, only logged via a {@link Logger}. - * - * @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 - */ - @CheckReturnValue - public <K1 extends K, V1 extends V> CacheBuilder<K1, V1> removalListener( - RemovalListener<? super K1, ? super V1> listener) { - checkState(this.removalListener == null); - - // safely limiting the kinds of caches this can produce - @SuppressWarnings("unchecked") - CacheBuilder<K1, V1> me = (CacheBuilder<K1, V1>) this; - me.removalListener = checkNotNull(listener); - return me; - } + @Override + public void invalidateAll() { + localCache.clear(); + } - // Make a safe contravariant cast now so we don't have to do it over and over. - @SuppressWarnings("unchecked") - <K1 extends K, V1 extends V> RemovalListener<K1, V1> getRemovalListener() { - return (RemovalListener<K1, V1>) Objects.firstNonNull(removalListener, NullListener.INSTANCE); - } + @Override + public long size() { + return localCache.size(); + } - /** - * Enable 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 stats requires - * bookkeeping to be performed with each operation, and thus imposes a performance penalty on - * cache operation. - * - * @since 12.0 (previously, stats collection was automatic) - */ - public CacheBuilder<K, V> recordStats() { - statsCounterSupplier = CACHE_STATS_COUNTER; - return this; + @Override + public ConcurrentMap<K, V> asMap() { + return localCache; + } } - Supplier<? extends StatsCounter> getStatsCounterSupplier() { - return statsCounterSupplier; - } + private static class LocalLoadingCache<K, V> + extends LocalManualCache<K, V> implements LoadingCache<K, V> { - /** - * 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. - * - * <p>This method does not alter the state of this {@code CacheBuilder} instance, so it can be - * invoked again to create multiple independent caches. - * - * @param loader the cache loader used to obtain new values - * @return a cache having the requested features - */ - public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build( - CacheLoader<? super K1, V1> loader) { - checkWeightWithWeigher(); - return new LocalCache.LocalLoadingCache<K1, V1>(this, loader); - } + LocalLoadingCache(CacheBuilder<? super K, ? super V> builder, + CacheLoader<? super K, V> loader) { + super(builder, checkNotNull(loader)); + } - /** - * Builds a cache which does not automatically load values when keys are requested. - * - * <p>Consider {@link #build(CacheLoader)} instead, if it is feasible to implement a - * {@code CacheLoader}. - * - * <p>This method does not alter the state of this {@code CacheBuilder} instance, so it can be - * invoked again to create multiple independent caches. - * - * @return a cache having the requested features - * @since 11.0 - */ - public <K1 extends K, V1 extends V> Cache<K1, V1> build() { - checkWeightWithWeigher(); - checkNonLoadingCache(); - return new LocalCache.LocalManualCache<K1, V1>(this); - } + // Cache methods - private void checkNonLoadingCache() { - checkState(refreshNanos == UNSET_INT, "refreshAfterWrite requires a LoadingCache"); - } + @Override + public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException { + throw new UnsupportedOperationException(); + } - private void checkWeightWithWeigher() { - if (weigher == null) { - checkState(maximumWeight == UNSET_INT, "maximumWeight requires weigher"); - } else { - if (strictParsing) { - checkState(maximumWeight != UNSET_INT, "weigher requires maximumWeight"); - } else { - if (maximumWeight == UNSET_INT) { - logger.log(Level.WARNING, "ignoring weigher specified without maximumWeight"); - } - } + @Override + public void refresh(K key) { + throw new UnsupportedOperationException(); } } - /** - * Returns a string representation for this CacheBuilder instance. The exact form of the returned - * string is not specified. - */ - @Override - public String toString() { - Objects.ToStringHelper s = Objects.toStringHelper(this); - if (initialCapacity != UNSET_INT) { - s.add("initialCapacity", initialCapacity); + // TODO(fry,user): ConcurrentHashMap never throws a CME when mutating the map during iteration, but + // this implementation (based on a LHM) does. This will all be replaced soon anyways, so leaving + // it as is for now. + private static class LocalCache<K, V> extends LinkedHashMap<K, V> + implements ConcurrentMap<K, V> { + private final CacheLoader<? super K, V> loader; + private final long expirationMillis; + private final int maximumSize; + + LocalCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { + super(builder.getInitialCapacity(), 0.75f, (builder.maximumSize != UNSET_INT)); + this.loader = loader; + this.expirationMillis = builder.getExpirationMillis(); + this.maximumSize = builder.maximumSize; } - if (concurrencyLevel != UNSET_INT) { - s.add("concurrencyLevel", concurrencyLevel); - } - if (maximumSize != UNSET_INT) { - s.add("maximumSize", maximumSize); + + @Override + public V put(K key, V value) { + V result = super.put(key, value); + if (expirationMillis > 0) { + scheduleRemoval(key, value); + } + return result; } - if (maximumWeight != UNSET_INT) { - s.add("maximumWeight", maximumWeight); + + @Override + protected boolean removeEldestEntry(Map.Entry<K, V> ignored) { + return (maximumSize == -1) ? false : size() > maximumSize; } - if (expireAfterWriteNanos != UNSET_INT) { - s.add("expireAfterWrite", expireAfterWriteNanos + "ns"); + + @Override + public V putIfAbsent(K key, V value) { + if (!containsKey(key)) { + return put(key, value); + } else { + return get(key); + } } - if (expireAfterAccessNanos != UNSET_INT) { - s.add("expireAfterAccess", expireAfterAccessNanos + "ns"); + + @Override + public boolean remove(Object key, Object value) { + if (containsKey(key) && get(key).equals(value)) { + remove(key); + return true; + } + return false; } - if (keyStrength != null) { - s.add("keyStrength", Ascii.toLowerCase(keyStrength.toString())); + + @Override + public boolean replace(K key, V oldValue, V newValue) { + if (containsKey(key) && get(key).equals(oldValue)) { + put(key, newValue); + return true; + } + return false; } - if (valueStrength != null) { - s.add("valueStrength", Ascii.toLowerCase(valueStrength.toString())); + + @Override + public V replace(K key, V value) { + return containsKey(key) ? put(key, value) : null; } - if (keyEquivalence != null) { - s.addValue("keyEquivalence"); + + private void scheduleRemoval(final K key, final V value) { + /* + * TODO: Keep weak reference to map, too. Build a priority queue out of the entries themselves + * instead of creating a task per entry. Then, we could have one recurring task per map (which + * would clean the entire map and then reschedule itself depending upon when the next + * expiration comes). We also want to avoid removing an entry prematurely if the entry was set + * to the same value again. + */ + Timer timer = new Timer() { + @Override + public void run() { + remove(key, value); + } + }; + timer.schedule((int) expirationMillis); } - if (valueEquivalence != null) { - s.addValue("valueEquivalence"); + + public V getOrLoad(Object k) throws ExecutionException { + // from CustomConcurrentHashMap + V result = super.get(k); + if (result == null) { + /* + * This cast isn't safe, but we can rely on the fact that K is almost always passed to + * Map.get(), and tools like IDEs and Findbugs can catch situations where this isn't the + * case. + * + * The alternative is to add an overloaded method, but the chances of a user calling get() + * instead of the new API and the risks inherent in adding a new API outweigh this little + * hole. + */ + @SuppressWarnings("unchecked") + K key = (K) k; + result = compute(key); + } + return result; } - if (removalListener != null) { - s.addValue("removalListener"); + + private V compute(K key) throws ExecutionException { + V value; + try { + value = loader.load(key); + } catch (RuntimeException e) { + throw new UncheckedExecutionException(e); + } catch (Exception e) { + throw new ExecutionException(e); + } catch (Error e) { + throw new ExecutionError(e); + } + + if (value == null) { + String message = loader + " returned null for key " + key + "."; + throw new InvalidCacheLoadException(message); + } + put(key, value); + return value; } - return s.toString(); } -} +} |