
com.pippsford.util.ConcurrentWeakValueMap Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of common-utils Show documentation
Show all versions of common-utils Show documentation
Code that seems to me to have utility across multiple projects
The newest version!
package com.pippsford.util;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.Spliterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* A map where the values are WeakReferences. This means that values can be garbage collected when no longer in use.
* Note that this map is thread-safe.
*
* @param the key type for this map
* @param the value type for this map
*
* @author Simon Greatrix
*/
public class ConcurrentWeakValueMap implements ConcurrentMap {
/**
* Iterator over map entries. The Iterator automatically skips enqueued referents.
*/
private abstract static class AbstractIterator implements Iterator {
/**
* Iterator on backing map.
*/
protected final Iterator>> iterator;
/**
* Current entry.
*/
protected ReferenceEntry current;
/**
* Next entry to return.
*/
protected ReferenceEntry next;
public AbstractIterator(
Iterator>> iterator
) {
this.iterator = iterator;
current = null;
getNext();
}
/**
* Get the nextEntry value, skipping enqueued referents.
*/
protected final void getNext() {
while (iterator.hasNext()) {
Entry> entry = iterator.next();
MapReference ref = entry.getValue();
V2 value = ref.get();
if (value != null) {
// have good value
next = new ReferenceEntry<>(entry, value);
return;
}
}
// no more entries
next = null;
}
/**
* Is there a nextEntry, non-enqueued, entry?.
*/
public boolean hasNext() {
return next != null;
}
/**
* Get the nextEntry entry. The return type depends on the type of this Iterator.
*/
protected Entry nextEntry() {
current = next;
if (current == null) {
throw new NoSuchElementException();
}
getNext();
return current;
}
/**
* Remove the current value from this map.
*/
public void remove() {
iterator.remove();
current = null;
}
}
/**
* Iterator over map keys. The Iterator automatically skips enqueued referents.
*/
private static class EntryIterator extends AbstractIterator> {
public EntryIterator(
Iterator>> iterator
) {
super(iterator);
}
/**
* Get the nextEntry entry from this map.
*/
// Do not need to explicitly throw NoSuchElementException as nextEntry() does that.
@SuppressWarnings("squid:S2272")
public Entry next() {
return nextEntry();
}
}
private static class EntrySpliterator implements Spliterator> {
/**
* Iterator on backing map.
*/
protected final Spliterator>> spliterator;
protected EntrySpliterator(Spliterator>> spliterator) {
this.spliterator = spliterator;
}
@Override
public int characteristics() {
// We are neither SIZED nor SUB-SIZED, as garbage collection may occur.
// Even if the underlying spliterator is SORTED, we cannot expose it as it operates on the wrong generic type.
return NONNULL | (spliterator.characteristics() & ~(SIZED | SUBSIZED | SORTED));
}
@Override
public long estimateSize() {
return spliterator.estimateSize();
}
@Override
public long getExactSizeIfKnown() {
// Garbage collection may occur, so the exact size is never known.
return -1L;
}
@Override
public boolean tryAdvance(final Consumer super Entry> action) {
final boolean[] searching = {true};
Consumer super Entry>> consumer = e -> {
MapReference ref = e.getValue();
V2 value = ref.get();
if (value != null) {
searching[0] = false;
action.accept(new ReferenceEntry<>(e, value));
}
};
do {
if (!spliterator.tryAdvance(consumer)) {
return false;
}
} while (searching[0]);
return true;
}
@Override
public Spliterator> trySplit() {
Spliterator>> split = spliterator.trySplit();
if (split != null) {
return new EntrySpliterator<>(split);
}
return null;
}
}
/**
* A WeakReference that knows which map key maps to its value. This allows the ReferenceQueue to remove the mapping
* from the map. Furthermore this reference
* will pretend to be equal to its value hiding the dereference operation when examining values in the map.
*/
public static class MapReference extends WeakReference {
/**
* The map key for this value.
*/
private final K2 key;
/**
* My myQueue.
*/
private final ReferenceQueue myQueue;
/**
* Create a new MapReference.
*
* @param queue the reference myQueue this will be registered with
* @param key the associated key
* @param val the referenced value
*/
public MapReference(ReferenceQueue queue, K2 key, V2 val) {
super(val, queue);
this.key = key;
myQueue = queue;
}
/**
* A MapReference is equal to another object if it is either a reference to the same object, or the object itself.
* This makes matching values easy.
*
* @param o the object to compare with for equality
*
* @return true if this equals the given object
*/
@Override
// It is OK to assign to parameters.
@SuppressWarnings("squid:S1226")
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null) {
return false;
}
if (o instanceof Reference>) {
Reference> ref = (Reference>) o;
o = ref.get();
if (o == null) {
return false;
}
}
return o.equals(get());
}
/**
* Get the key this was mapped from.
*
* @return the associated key
*/
public K2 getKey() {
return key;
}
/**
* We use the same hashCode as the value.
*
* @return the value's hash code
*/
@Override
public int hashCode() {
Object val = get();
return (val == null) ? 0 : val.hashCode();
}
/**
* Create a new MapReference mapped to the same key as this and registered in the specified ReferenceQueue.
*
* @param newVal the new value for this mapping
*
* @return a reference for the new value
*/
public MapReference newReference(V2 newVal) {
if (newVal == null) {
throw new NullReferenceException();
}
return new MapReference<>(myQueue, key, newVal);
}
/**
* This reference's value as a string.
*
* @return this reference's value as a string
*/
@Override
public String toString() {
return String.valueOf(get());
}
}
private static class NullReferenceException extends IllegalArgumentException {
NullReferenceException() {
super("Cannot have reference to null.");
}
}
/**
* An entry in the map. To prevent garbage collection, the entry maintains a strong reference to its value.
*/
private static class ReferenceEntry implements Entry {
/**
* The original entry in the backing map.
*/
final Entry> entry;
/**
* The strong reference to the value.
*/
V value;
/**
* Create a new ReferenceEntry.
*/
public ReferenceEntry(Entry> entry, V value) {
this.entry = entry;
this.value = value;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Entry, ?>)) {
return false;
}
Entry, ?> e = (Entry, ?>) o;
return Objects.equals(e.getKey(), entry.getKey()) && Objects.equals(e.getValue(), value);
}
/**
* Get the map key for this entry.
*/
public K getKey() {
return entry.getKey();
}
/**
* Get the map value for this entry.
*/
public V getValue() {
return value;
}
@Override
public int hashCode() {
return entry.hashCode();
}
/**
* Set the map value for this entry.
*/
public V setValue(V newValue) {
MapReference ref = entry.getValue();
MapReference newRef = ref.newReference(newValue);
entry.setValue(newRef);
V oldVal = value;
value = newValue;
return oldVal;
}
public String toString() {
return entry.getKey() + "=" + value;
}
}
/**
* Iterator over map entries. The Iterator automatically skips enqueued referents.
*/
private static class ValueIterator extends AbstractIterator {
public ValueIterator(
Iterator>> iterator
) {
super(iterator);
}
/**
* Get the nextEntry value from this map.
*/
// Do not need to explicitly throw NoSuchElementException as nextEntry() does that.
@SuppressWarnings("squid:S2272")
public V2 next() {
Entry e = nextEntry();
return e.getValue();
}
}
private static class ValueSpliterator implements Spliterator {
/**
* Iterator on backing map.
*/
protected final Spliterator> spliterator;
protected ValueSpliterator(Spliterator> spliterator) {
this.spliterator = spliterator;
}
@Override
public int characteristics() {
// We are neither SIZED nor SUB-SIZED, as garbage collection may occur.
// Even if the underlying spliterator is SORTED, we cannot expose it as it operates on the wrong generic type.
return NONNULL | (spliterator.characteristics() & ~(SIZED | SUBSIZED | SORTED));
}
@Override
public long estimateSize() {
return spliterator.estimateSize();
}
@Override
public long getExactSizeIfKnown() {
// Garbage collection may occur, so the exact size is never known.
return -1L;
}
@Override
public boolean tryAdvance(final Consumer super V2> action) {
final boolean[] searching = {true};
Consumer super MapReference> consumer = ref -> {
V2 value = ref.get();
if (value != null) {
searching[0] = false;
action.accept(value);
}
};
do {
if (!spliterator.tryAdvance(consumer)) {
return false;
}
} while (searching[0]);
return true;
}
@Override
public Spliterator trySplit() {
Spliterator> split = spliterator.trySplit();
if (split != null) {
return new ValueSpliterator<>(split);
}
return null;
}
}
/**
* Get an array from a collection. Special handling is needed to skip enqueued referents.
*
* @param coll the collection to process
* @param array the candidate destination array
* @param the type of array to return
*
* @return an array containing everything in the collection
*/
// It is OK to assign to parameters.
@SuppressWarnings({"squid:S1226"})
// We have to be sure the correct iterator is used to handle enqueued referents.
@SuppressFBWarnings("UAA_USE_ADD_ALL")
static T[] toArray(Collection extends T> coll, T[] array) {
// Get all the objects in the collection.
// The size may change, so we get the objects into blocks
// and put the blocks into a list
List blocks = new ArrayList<>(coll.size());
Iterator extends T> itr = coll.iterator();
while (itr.hasNext()) {
blocks.add(itr.next());
}
int size = blocks.size();
// ensure the destination is big enough
if (array.length < size) {
@SuppressWarnings("unchecked")
T[] newArray = (T[]) java.lang.reflect.Array.newInstance(
array.getClass().getComponentType(), size);
array = newArray;
}
// copy the objects out of the blocks into the destination
int i = 0;
itr = blocks.iterator();
while (itr.hasNext()) {
array[i] = itr.next();
i++;
}
// put in trailing null if necessary
if (i < array.length) {
array[i] = null;
}
return array;
}
/**
* Set of Entries in this map.
*/
protected class Entries extends RefSet> {
/**
* {@inheritDoc}
*/
@Override
public boolean contains(Object o) {
if (o == null) {
return false;
}
if (!(o instanceof Map.Entry, ?>)) {
return false;
}
Entry, ?> e = (Entry, ?>) o;
Object val = get(e.getKey());
return (val != null) && (val.equals(e.getValue()));
}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public Iterator> iterator() {
return new EntryIterator<>(map.entrySet().iterator());
}
/**
* {@inheritDoc}
*/
@Override
public boolean remove(Object o) {
if (o == null) {
return false;
}
if (!(o instanceof Map.Entry, ?>)) {
return false;
}
Entry, ?> e = (Entry, ?>) o;
return ConcurrentWeakValueMap.this.remove(e.getKey()) != null;
}
@Override
public Spliterator> spliterator() {
return new EntrySpliterator<>(map.entrySet().spliterator());
}
}
/**
* Abstract set of keys or entries in this map.
*/
private abstract class RefSet extends AbstractSet {
protected RefSet() {
// do nothing
}
/**
* {@inheritDoc}
*/
@Override
public void clear() {
ConcurrentWeakValueMap.this.clear();
}
/**
* {@inheritDoc}
*/
@Override
public int size() {
return ConcurrentWeakValueMap.this.size();
}
/**
* Get the objects in this set as an array. Note that enqueued referents will be skipped.
*/
@Nonnull
@Override
public Object[] toArray() {
return toArray(new Object[0]);
}
/**
* Get the objects in this set as an array. Note that enqueued referents will be skipped, so the returned array may
* have less values than the size() would
* indicate. As per the Map API if the supplied array is too large, there will be a trailing null after all this
* Set's objects.
*/
@Override
@Nonnull
public T[] toArray(@Nonnull T[] array) {
@SuppressWarnings("unchecked")
T[] newArray = (T[]) ConcurrentWeakValueMap.toArray(this, array);
return newArray;
}
}
/**
* Collection of values in this map.
*/
protected class Values extends AbstractCollection {
/**
* {@inheritDoc}
*/
@Override
public void clear() {
ConcurrentWeakValueMap.this.clear();
}
/**
* {@inheritDoc}
*/
@Override
public boolean contains(Object o) {
return containsValue(o);
}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public Iterator iterator() {
return new ValueIterator<>(map.entrySet().iterator());
}
/**
* {@inheritDoc}
*/
@Override
public int size() {
return ConcurrentWeakValueMap.this.size();
}
@Override
public Spliterator spliterator() {
return new ValueSpliterator<>(map.values().spliterator());
}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public T[] toArray(@Nonnull T[] array) {
@SuppressWarnings("unchecked")
T[] newArray = (T[]) ConcurrentWeakValueMap.toArray(this, array);
return newArray;
}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public Object[] toArray() {
return toArray(new Object[0]);
}
}
/**
* The backing map.
*/
protected final ConcurrentMap> map;
/**
* The reference myQueue where expired values will be placed.
*/
private final ReferenceQueue queue = new ReferenceQueue<>();
/**
* Entries set, initialised lazily.
*/
private Set> entries = null;
/**
* Values collection, initialised lazily.
*/
private Collection values = null;
/**
* Create new WeakValueMap backed by a HashMap and allowing clean-up on read.
*/
public ConcurrentWeakValueMap() {
this(new ConcurrentHashMap<>());
}
/**
* Create new WeakValueMap backed by the specified map and allowing clean-up on read.
*
* @param map the map to initialise from
*/
public ConcurrentWeakValueMap(ConcurrentMap> map) {
this.map = map;
}
/**
* Trigger clean-up of the map. This should be called if the map is not being written to and clean-up on read is not
* allowed.
* This method is called automatically on every write operation and on every read operation if clean-up on read is
* allowed.
*/
public void cleanUp() {
while (true) {
@SuppressWarnings("unchecked")
MapReference ref = (MapReference) queue.poll();
if (ref == null) {
break;
}
K key = ref.getKey();
MapReference val = map.get(key);
if (val == ref) {
map.remove(key);
notifyCleanUp(key);
}
}
}
/**
* Clear this map.
*
* @see Map#clear()
*/
public void clear() {
cleanUp();
map.clear();
}
/**
* Does this map contain the specified key?.
*
* @param key the key to check
*
* @return true if this map contains a mapping for the key
*
* @see Map#containsValue(Object)
*/
public boolean containsKey(Object key) {
cleanUp();
return map.containsKey(key);
}
/**
* Does this map contain the specified value?.
*
* @param value the value to check for
*
* @return true if this map contains a mapping to the value
*
* @see Map#containsValue(Object)
*/
public boolean containsValue(Object value) {
cleanUp();
// we can't contain null, so return false now and avoid NPE later
if (value == null) {
return false;
}
// two MapRefs are equal if they point to the same value, so
// create a new ref with a null key to match on
MapReference ref = new MapReference<>(null, null,
value
);
return map.containsValue(ref);
}
/**
* Does this map contain the specified value?.
*
* @return true if this map contains a mapping to the value
*
* @see Map#containsValue(Object)
*/
@Nonnull
public Set> entrySet() {
cleanUp();
Set> es = entries;
if (es != null) {
return es;
}
entries = new Entries();
return entries;
}
/**
* Get the object mapped to the specified key.
*
* @param key the key to get the mapping for
*
* @return the value mapped to the given key, or null if not found.
*
* @see Map#get(Object)
*/
public V get(Object key) {
cleanUp();
MapReference ref = map.get(key);
return (ref != null) ? ref.get() : null;
}
/**
* Is this map empty?.
*
* @return true if this map is empty
*
* @see Map#isEmpty()
*/
public boolean isEmpty() {
cleanUp();
return map.isEmpty();
}
/**
* Get the set of all keys for this map.
*
* @return set of all keys in this map.
*
* @see Map#keySet()
*/
@Nonnull
public Set keySet() {
cleanUp();
return map.keySet();
}
/**
* Invoked when the garbage collection of a key has been detected and its key mapping removed. The default
* implementation does nothing.
*
* @param key the key whose value was GCed.
*/
protected void notifyCleanUp(K key) {
// do nothing
}
/**
* Put the given mapping into this map.
*
* @param key the key for the mapping
* @param value the value the key is mapped to
*
* @return the previous mapping for this key, or null if none
*
* @see Map#put(Object, Object)
*/
public V put(K key, V value) {
cleanUp();
if (value == null) {
throw new NullReferenceException();
}
MapReference ref = new MapReference<>(queue, key, value);
MapReference ret = map.put(key, ref);
return (ret != null) ? ret.get() : null;
}
/**
* Put all the mappings in the specified map into this map.
*
* @param t the map to copy into this
*
* @see Map#putAll(Map)
*/
public void putAll(@Nonnull Map extends K, ? extends V> t) {
cleanUp();
for (Entry extends K, ? extends V> e : t.entrySet()) {
K k = e.getKey();
V v = e.getValue();
put(k, v);
}
}
@Override
public V putIfAbsent(K key, V value) {
cleanUp();
if (value == null) {
throw new NullReferenceException();
}
MapReference ref = new MapReference<>(queue, key, value);
MapReference ret = map.putIfAbsent(key, ref);
return (ret != null) ? ret.get() : null;
}
@Override
public boolean remove(Object key, Object value) {
cleanUp();
// Cannot have null values
if (value == null) {
return false;
}
MapReference ref = map.get(key);
if (ref == null) {
return false;
}
if (value.equals(ref.get())) {
return map.remove(key, ref);
}
return false;
}
/**
* Remove a mapping from the map.
*
* @param key the key for the mapping to remove
*
* @return the value the key was mapped to, or null if none
*
* @see Map#remove(Object)
*/
public V remove(Object key) {
cleanUp();
MapReference ret = map.remove(key);
return (ret != null) ? ret.get() : null;
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
if (oldValue == null) {
return false;
}
if (newValue == null) {
throw new NullReferenceException();
}
MapReference oldRef = new MapReference<>(queue, key, oldValue);
MapReference newRef = new MapReference<>(queue, key, newValue);
return map.replace(key, oldRef, newRef);
}
@Override
public V replace(K key, V value) {
if (value == null) {
throw new NullReferenceException();
}
cleanUp();
MapReference oldRef = map.get(key);
if (oldRef == null) {
return null;
}
V oldValue = oldRef.get();
if (oldValue != null) {
MapReference newRef = new MapReference<>(queue, key, value);
oldRef = map.replace(key, newRef);
// another thread may have removed or changed the reference
if (oldRef == null) {
return null;
}
V oldValue2 = oldRef.get();
if (oldValue2 != null) {
oldValue = oldValue2;
}
}
return oldValue;
}
/**
* Get number of entries in this map.
*
* @return number of entries in this map.
*
* @see Map#size()
*/
public int size() {
cleanUp();
return map.size();
}
/**
* Get the collection of all values in this map.
*
* @return collection all values in this map
*
* @see Map#values()
*/
@Nonnull
public Collection values() {
cleanUp();
Collection vs = values;
if (vs != null) {
return vs;
}
values = new Values();
return values;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy