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

com.google.common.collect.MapMaker Maven / Gradle / Ivy

There is a newer version: 3.0.0.Alpha1
Show newest version
/*
 * Copyright (C) 2009 Google Inc.
 *
 * 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.google.common.collect;

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.annotations.GwtIncompatible;
import com.google.common.base.Equivalence;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.collect.CustomConcurrentHashMap.Strength;

import java.io.Serializable;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

/**
 * 

A {@link ConcurrentMap} builder, providing any combination of these * features: {@linkplain SoftReference soft} or {@linkplain WeakReference * weak} keys, soft or weak values, timed expiration, and on-demand * computation of values. Usage example:

 {@code
 *
 *   ConcurrentMap graphs = new MapMaker()
 *       .concurrencyLevel(32)
 *       .softKeys()
 *       .weakValues()
 *       .expiration(30, TimeUnit.MINUTES)
 *       .makeComputingMap(
 *           new Function() {
 *             public Graph apply(Key key) {
 *               return createExpensiveGraph(key);
 *             }
 *           });}
* * These features are all optional; {@code new MapMaker().makeMap()} * returns a valid concurrent map that behaves exactly like a * {@link ConcurrentHashMap}. * * The returned map is implemented as a hash table with similar performance * characteristics to {@link ConcurrentHashMap}. It supports all optional * operations of the {@code ConcurrentMap} interface. It does not permit * null keys or values. It is serializable; however, serializing a map that * uses soft or weak references can give unpredictable results. * *

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

The returned map has weakly consistent iteration: an iterator * over one of the map's view collections may reflect some, all or none of * the changes made to the map after the iterator was created. * *

An entry whose key or value is reclaimed by the garbage collector * immediately disappears from the map. (If the default settings of strong * keys and strong values are used, this will never happen.) The client can * never observe a partially-reclaimed entry. Any {@link java.util.Map.Entry} * instance retrieved from the map's {@linkplain Map#entrySet() entry set} * is a snapshot of that entry's state at the time of retrieval; such entries * do, however, support {@link java.util.Map.Entry#setValue}. * *

{@code new MapMaker().weakKeys().makeMap()} can almost always be * used as a drop-in replacement for {@link java.util.WeakHashMap}, adding * concurrency, asynchronous cleanup, identity-based equality for keys, and * great flexibility. * * @author Bob Lee * @author Kevin Bourrillion * @since 2 (imported from Google Collections Library) */ @GwtCompatible(emulated = true) public final class MapMaker { private static final int DEFAULT_INITIAL_CAPACITY = 16; private static final int DEFAULT_CONCURRENCY_LEVEL = 16; private static final int DEFAULT_EXPIRATION_NANOS = 0; private static final int UNSET_INITIAL_CAPACITY = -1; private static final int UNSET_CONCURRENCY_LEVEL = -1; static final int UNSET_EXPIRATION_NANOS = -1; static final int UNSET_MAXIMUM_SIZE = -1; int initialCapacity = UNSET_INITIAL_CAPACITY; int concurrencyLevel = UNSET_CONCURRENCY_LEVEL; int maximumSize = UNSET_MAXIMUM_SIZE; Strength keyStrength; Strength valueStrength; long expirationNanos = UNSET_EXPIRATION_NANOS; private boolean useCustomMap; Equivalence keyEquivalence; Equivalence valueEquivalence; /** * Constructs a new {@code MapMaker} instance with default settings, * including strong keys, strong values, and no automatic expiration. */ public MapMaker() {} // TODO: undo this indirection if keyEquiv gets released MapMaker privateKeyEquivalence(Equivalence equivalence) { checkState(keyEquivalence == null, "key equivalence was already set to " + keyEquivalence); keyEquivalence = checkNotNull(equivalence); this.useCustomMap = true; return this; } Equivalence getKeyEquivalence() { return Objects.firstNonNull(keyEquivalence, getKeyStrength().defaultEquivalence()); } // TODO: undo this indirection if valueEquiv gets released MapMaker privateValueEquivalence(Equivalence equivalence) { checkState(valueEquivalence == null, "value equivalence was already set to " + valueEquivalence); this.valueEquivalence = checkNotNull(equivalence); this.useCustomMap = true; return this; } Equivalence getValueEquivalence() { return Objects.firstNonNull(valueEquivalence, getValueStrength().defaultEquivalence()); } /** * Sets a custom initial capacity (defaults to 16). Resizing this or * any other kind of hash table is a relatively slow operation, so, * when possible, it is a good idea to provide estimates of expected * table sizes. * * @throws IllegalArgumentException if {@code initialCapacity} is * negative * @throws IllegalStateException if an initial capacity was already set */ public MapMaker initialCapacity(int initialCapacity) { checkState(this.initialCapacity == UNSET_INITIAL_CAPACITY, "initial capacity was already set to " + this.initialCapacity); checkArgument(initialCapacity >= 0); this.initialCapacity = initialCapacity; return this; } int getInitialCapacity() { return (initialCapacity == UNSET_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : initialCapacity; } /** * Specifies the maximum number of entries the map may contain. While the * number of entries in the map is not guaranteed to grow to the maximum, * the map will attempt to make the best use of memory without exceeding the * maximum number of entries. As the map size grows close to the maximum, * the map will evict entries that are less likely to be used again. For * example, the map may evict an entry because it hasn't been used recently * or very often. * * @throws IllegalArgumentException if {@code size} is negative * @throws IllegalStateException if a maximum size was already set */ // TODO: Implement and make public. MapMaker maximumSize(int size) { // TODO: Should we disallow maximumSize < concurrencyLevel? If we allow it, // should we return a dummy map that doesn't actually retain any // entries? checkState(this.maximumSize == UNSET_MAXIMUM_SIZE, "maximum size was already set to " + this.maximumSize); checkArgument(initialCapacity >= 0); this.maximumSize = size; this.useCustomMap = true; return this; } /** * 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 placement in hash tables is essentially random, * the actual concurrency will 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 * is appropriate when it is known that only one thread will modify and * all others will only read. Defaults to 16. * * @throws IllegalArgumentException if {@code concurrencyLevel} is * nonpositive * @throws IllegalStateException if a concurrency level was already set */ @GwtIncompatible("java.util.concurrent.ConcurrentHashMap concurrencyLevel") public MapMaker concurrencyLevel(int concurrencyLevel) { checkState(this.concurrencyLevel == UNSET_CONCURRENCY_LEVEL, "concurrency level was already set to " + this.concurrencyLevel); checkArgument(concurrencyLevel > 0); this.concurrencyLevel = concurrencyLevel; return this; } int getConcurrencyLevel() { return (concurrencyLevel == UNSET_CONCURRENCY_LEVEL) ? DEFAULT_CONCURRENCY_LEVEL : concurrencyLevel; } /** * Specifies that each key (not value) stored in the map should be * wrapped in a {@link WeakReference} (by default, strong references * are used). * *

Note: the map will use identity ({@code ==}) comparison * to determine equality of weak keys, which may not behave as you expect. * For example, storing a key in the map and then attempting a lookup * using a different but {@link Object#equals(Object) equals}-equivalent * key will always fail. * * @throws IllegalStateException if the key strength was already set * @see WeakReference */ @GwtIncompatible("java.lang.ref.WeakReference") public MapMaker weakKeys() { return setKeyStrength(Strength.WEAK); } /** * Specifies that each key (not value) stored in the map should be * wrapped in a {@link SoftReference} (by default, strong references * are used). * *

Note: the map will use identity ({@code ==}) comparison * to determine equality of soft keys, which may not behave as you expect. * For example, storing a key in the map and then attempting a lookup * using a different but {@link Object#equals(Object) equals}-equivalent * key will always fail. * * @throws IllegalStateException if the key strength was already set * @see SoftReference */ @GwtIncompatible("java.lang.ref.SoftReference") public MapMaker softKeys() { return setKeyStrength(Strength.SOFT); } MapMaker setKeyStrength(Strength strength) { checkState(keyStrength == null, "Key strength was already set to " + keyStrength + "."); keyStrength = checkNotNull(strength); if (strength != Strength.STRONG) { // STRONG could be used during deserialization. useCustomMap = true; } return this; } Strength getKeyStrength() { return Objects.firstNonNull(keyStrength, Strength.STRONG); } /** * Specifies that each value (not key) stored in the map 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: the map will use identity ({@code ==}) comparison * to determine equality of weak values. This will notably impact * the behavior of {@link Map#containsValue(Object) containsValue}, * {@link ConcurrentMap#remove(Object, Object) remove(Object, Object)}, * and {@link ConcurrentMap#replace(Object, Object, Object) replace(K, V, V)}. * * @throws IllegalStateException if the key strength was already set * @see WeakReference */ @GwtIncompatible("java.lang.ref.WeakReference") public MapMaker weakValues() { return setValueStrength(Strength.WEAK); } /** * Specifies that each value (not key) stored in the map should be * wrapped in a {@link SoftReference} (by default, strong references * are used). * *

Soft values will be garbage collected in response to memory * demand, and in a least-recently-used manner. This makes them a * good candidate for caching. * *

Note: the map will use identity ({@code ==}) comparison * to determine equality of soft values. This will notably impact * the behavior of {@link Map#containsValue(Object) containsValue}, * {@link ConcurrentMap#remove(Object, Object) remove(Object, Object)}, * and {@link ConcurrentMap#replace(Object, Object, Object) replace(K, V, V)}. * * @throws IllegalStateException if the value strength was already set * @see SoftReference */ @GwtIncompatible("java.lang.ref.SoftReference") public MapMaker softValues() { return setValueStrength(Strength.SOFT); } MapMaker setValueStrength(Strength strength) { checkState(valueStrength == null, "Value strength was already set to " + valueStrength + "."); valueStrength = checkNotNull(strength); if (strength != Strength.STRONG) { // STRONG could be used during deserialization. useCustomMap = true; } return this; } Strength getValueStrength() { return Objects.firstNonNull(valueStrength, Strength.STRONG); } /** * Specifies that each entry should be automatically removed from the * map once a fixed duration has passed since the entry's creation. * Note that changing the value of an entry will reset its expiration * time. * * @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 not positive * @throws IllegalStateException if the expiration time was already set */ public MapMaker expiration(long duration, TimeUnit unit) { checkState(expirationNanos == UNSET_EXPIRATION_NANOS, "expiration time of " + expirationNanos + " ns was already set"); checkArgument(duration > 0, "invalid duration: " + duration); this.expirationNanos = unit.toNanos(duration); useCustomMap = true; return this; } long getExpirationNanos() { return (expirationNanos == UNSET_EXPIRATION_NANOS) ? DEFAULT_EXPIRATION_NANOS : expirationNanos; } /** * Builds a map, without on-demand computation of values. This method * does not alter the state of this {@code MapMaker} instance, so it can be * invoked again to create multiple independent maps. * * @param the type of keys to be stored in the returned map * @param the type of values to be stored in the returned map * @return a serializable concurrent map having the requested features */ public ConcurrentMap makeMap() { return useCustomMap ? new CustomConcurrentHashMap(this) : new ConcurrentHashMap(getInitialCapacity(), 0.75f, getConcurrencyLevel()); } /** * Builds a caching function, which either returns an already-computed value * for a given key or atomically computes it using the supplied function. * If another thread is currently computing the value for this key, simply * waits for that thread to finish and returns its computed value. Note that * the function may be executed concurrently by multiple threads, but only for * distinct keys. * *

The {@code Map} view of the {@code Cache}'s cache is only * updated when function computation completes. In other words, an entry isn't * visible until the value's computation completes. No methods on the {@code * Map} will ever trigger computation. * *

{@link Cache#apply} in the returned function implementation may * throw: * *

    *
  • {@link NullPointerException} if the key is null or the * computing function returns null *
  • {@link ComputationException} if an exception was thrown by the * computing function. If that exception is already of type {@link * ComputationException} it is propagated directly; otherwise it is * wrapped. *
* *

If {@link Map#put} is called on the underlying map before a computation * completes, other threads waiting on the computation will wake up and return * the stored value. When the computation completes, its new result will * overwrite the value that was put in the map manually. * *

This method does not alter the state of this {@code MapMaker} instance, * so it can be invoked again to create multiple independent maps. * * @param the type of keys to be stored in the returned cache * @param the type of values to be stored in the returned cache * @return a serializable cache having the requested features */ // TODO: figure out the Cache interface first Cache makeCache( Function computingFunction) { return new ComputingConcurrentHashMap(this, computingFunction); } /** * Builds a map that supports atomic, on-demand computation of values. {@link * Map#get} either returns an already-computed value for the given key, * atomically computes it using the supplied function, or, if another thread * is currently computing the value for this key, simply waits for that thread * to finish and returns its computed value. Note that the function may be * executed concurrently by multiple threads, but only for distinct keys. * *

If an entry's value has not finished computing yet, query methods * besides {@code get} return immediately as if an entry doesn't exist. In * other words, an entry isn't externally visible until the value's * computation completes. * *

{@link Map#get} on the returned map will never return {@code null}. It * may throw: * *

    *
  • {@link NullPointerException} if the key is null or the computing * function returns null *
  • {@link ComputationException} if an exception was thrown by the * computing function. If that exception is already of type {@link * ComputationException} it is propagated directly; otherwise it is * wrapped. *
* *

Note: Callers of {@code get} must ensure that the key * argument is of type {@code K}. The {@code get} method accepts {@code * Object}, so the key type is not checked at compile time. Passing an object * of a type other than {@code K} can result in that object being unsafely * passed to the computing function as type {@code K}, and unsafely stored in * the map. * *

If {@link Map#put} is called before a computation completes, other * threads waiting on the computation will wake up and return the stored * value. When the computation completes, its new result will overwrite the * value that was put in the map manually. * *

This method does not alter the state of this {@code MapMaker} instance, * so it can be invoked again to create multiple independent maps. */ public ConcurrentMap makeComputingMap( Function computingFunction) { Cache cache = makeCache(computingFunction); return new ComputingMapAdapter(cache); } /** * A function which caches the result of each application (computation). This * interface does not specify the caching semantics, but does expose a {@code * ConcurrentMap} view of cached entries. * * @author Bob Lee */ interface Cache extends Function { /** * Returns a map view of the cached entries. */ ConcurrentMap asMap(); } /** * Overrides get() to compute on demand. */ static class ComputingMapAdapter extends ForwardingConcurrentMap implements Serializable { private static final long serialVersionUID = 0; final Cache cache; ComputingMapAdapter(Cache cache) { this.cache = cache; } @Override protected ConcurrentMap delegate() { return cache.asMap(); } @SuppressWarnings("unchecked") // unsafe, which is why this is deprecated @Override public V get(Object key) { return cache.apply((K) key); } } }