// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.collections; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** * A hashmap wrapper which defers cloning of the enclosed map until it is written to. * Use this to make clones cheap in maps which are often not further modified. *

* As with regular maps, this can only be used safely if the content of the map is immutable. * If not, the {@link #copyMap} method can be overridden to perform a deep clone. * * @author bratseth */ public class CopyOnWriteHashMap extends AbstractMap implements Cloneable { private Map map; /** This class may write to the map if it is the sole owner */ private AtomicInteger owners = new AtomicInteger(1); /** Lazily initialized view */ private transient Set> entrySet = null; public CopyOnWriteHashMap() { this.map = new HashMap<>(); } public CopyOnWriteHashMap(int capacity) { this.map = new HashMap<>(capacity); } public CopyOnWriteHashMap(Map map) { this.map = new HashMap<>(map); } private void makeReadOnly() { owners.incrementAndGet(); } private boolean isWritable() { return owners.get() == 1; } private void makeWritable() { if (isWritable()) return; map = copyMap(map); owners.decrementAndGet(); owners = new AtomicInteger(1); entrySet = null; } /** * Make a copy of the given map with the requisite deepness. * This default implementation does return new HashMap<>(original); */ protected Map copyMap(Map original) { return new HashMap<>(original); } @SuppressWarnings("unchecked") public CopyOnWriteHashMap clone() { try { CopyOnWriteHashMap clone = (CopyOnWriteHashMap)super.clone(); makeReadOnly(); // owners shared with clone return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } @Override public Set> entrySet() { if (entrySet == null) entrySet = new EntrySet(); return entrySet; } @Override public V put(K key, V value) { makeWritable(); return map.put(key, value); } /** Override to avoid using iterator.remove */ @Override public V remove(Object key) { makeWritable(); return map.remove(key); } @Override public boolean equals(Object other) { if ( ! (other instanceof CopyOnWriteHashMap)) return false; return this.map.equals(((CopyOnWriteHashMap)other).map); } @Override public int hashCode() { return map.hashCode(); } private final class EntrySet extends AbstractSet> { public Iterator> iterator() { return new EntryIterator(); } @SuppressWarnings("unchecked") public boolean contains(Object o) { if ( ! (o instanceof Map.Entry)) return false; Map.Entry entry = (Map.Entry) o; Object candidate = map.get(entry.getKey()); if (candidate == null) return entry.getValue()==null; return candidate.equals(entry.getValue()); } public boolean remove(Object o) { makeWritable(); return map.remove(o) !=null; } public int size() { return map.size(); } public void clear() { map.clear(); } } /** * An entry iterator which does not allow removals if the map wasn't already modifiable * There is no sane way to implement that given that the wrapped map changes mid iteration. */ private class EntryIterator implements Iterator> { /** Wrapped iterator */ private final Iterator> mapIterator; public EntryIterator() { mapIterator = map.entrySet().iterator(); } public final boolean hasNext() { return mapIterator.hasNext(); } public Entry next() { return mapIterator.next(); } public void remove() { if ( ! isWritable()) throw new UnsupportedOperationException("Cannot perform the copy-on-write operation during iteration"); mapIterator.remove(); } } }