/* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package fr.neatmonster.nocheatplus.utilities.ds.map; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Lock on write hash map. Less jumpy than cow, bucket oriented addition, bulk * remove. Does not implement the Map interface, due to it not being right for * this purpose. *
* Field of use should be a thread-safe map, where get is not using locking and * where bulk (removal) operations can be performed with one time locking, while * entries usually stay for a longer time until expired. *
* All changes are still done under lock. Iterators are meant to iterate * fail-safe, even on concurrent modification of the map. Calling remove() on an * iterator should relay to originalMap.remove(item). The internal size may not * ever shrink, at least not below targetSize, but it might grow with entries.
* Both null keys and null values are supported. * * @author asofold * */ public class HashMapLOW { /////////////////////// // Static members /////////////////////// private static int getHashCode(final K key) { return key == null ? 0 : key.hashCode(); } private static int getBucketIndex(final int hashCode, final int buckets) { return Math.abs(hashCode) % buckets; } static class LHMEntry implements Entry { final int hashCode; final K key; V value; LHMEntry(int hashCode, K key, V value) { this.hashCode = hashCode; this.key = key; this.value = value; } @Override public K getKey() { return key; } @Override public V getValue() { return value; } @Override public V setValue(V value) { final V oldValue = this.value; this.value = value; return oldValue; } @Override public int hashCode() { // By specification, not intended to be useful here. return hashCode ^ (value == null ? 0 : value.hashCode()); } @Override public boolean equals(Object obj) { // By specification, not intended to be useful here. if (obj instanceof Entry) { final Entry entry = (Entry) obj; return obj == this || (key == null ? entry.getKey() == null : key.equals(entry.getKey())) && (value == null ? entry.getValue() == null : value.equals(entry.getValue())); } else { return false; } } /** * Delegate key comparison here, using a pre-calculated hash value. * * @param otherHashCode * @param otherKey * @return */ public boolean equalsKey(final int otherHashCode, final K otherKey) { if (otherHashCode != hashCode) { return false; } if (otherKey == key) { return true; } // (One of both still could be null.) return key != null && key.equals(otherKey); } } /** * Create with adding entries. * @author asofold * * @param * @param */ static class LHMBucket { // TODO: Link non-empty buckets. // TODO: final int index; int size = 0; /** Must be stored externally for iteration. */ @SuppressWarnings("unchecked") LHMEntry[] contents = (LHMEntry[]) new LHMEntry[3]; // TODO: Configurable /** * Called under lock. * * @param key * @param value * @param ifAbsent * If true, an existing non-null (!) value will not be * overridden. * @return */ V put(final int hashCode, final K key, final V value, final boolean ifAbsent) { int emptyIndex; if (size == 0) { emptyIndex = 0; size ++; } else { emptyIndex = -1; LHMEntry oldEntry = null; int entriesFound = 0; for (int i = 0; i < contents.length; i++) { final LHMEntry entry = contents[i]; if (entry != null) { entriesFound ++; if (entry.equalsKey(hashCode, key)) { oldEntry = entry; break; } else if (entriesFound == size && emptyIndex != -1) { // TODO: Not sure this is just overhead for most cases. break; } } else if (emptyIndex == -1) { emptyIndex = i; } } if (oldEntry != null) { final V oldValue = oldEntry.getValue(); if (oldValue == null || !ifAbsent) { oldEntry.setValue(value); } return oldValue; } } // Create a new Entry. final LHMEntry newEntry = new LHMEntry(hashCode, key, value); if (emptyIndex == -1) { // Grow. grow(newEntry); } else { contents[emptyIndex] = newEntry; } size ++; return null; } /** * Called under lock. * @param entry The entry to add (reason for growth). */ private void grow(final LHMEntry entry) { final int oldLength = contents.length; @SuppressWarnings("unchecked") LHMEntry[] newContents = (LHMEntry[]) new LHMEntry[contents.length + Math.max(2, contents.length / 3)]; System.arraycopy(contents, 0, newContents, 0, contents.length); newContents[oldLength] = entry; contents = newContents; } /** * Blind adding of the entry to a free place. Called under lock. * * @param entry */ void addEntry(LHMEntry entry) { size ++; for (int i = 0; i < contents.length; i++) { if (contents[i] == null) { contents[i] = entry; return; } } // Need to grow. grow(entry); } /** * Called under lock. * @param hashCode * @param key * @return */ V remove(final int hashCode, final K key) { if (size == 0) { return null; } else { for (int i = 0; i < contents.length; i++) { final LHMEntry entry = contents[i]; if (entry != null && entry.equalsKey(hashCode, key)) { contents[i] = null; size --; return entry.getValue(); } } return null; } } /** * Not necessarily called under lock. * @param hashCode * @param key * @return */ V get(final int hashCode, final K key) { final LHMEntry[] contents = this.contents; // Mind iteration. if (size == 0) { return null; } else { for (int i = 0; i < contents.length; i++) { final LHMEntry entry = contents[i]; if (entry != null && entry.equalsKey(hashCode, key)) { return entry.getValue(); } } return null; } } /** * Not necessarily called under lock. * @param hashCode * @param key * @return */ boolean containsKey(final int hashCode, final K key) { final LHMEntry[] contents = this.contents; // Mind iteration. if (size == 0) { return false; } else { for (int i = 0; i < contents.length; i++) { final LHMEntry entry = contents[i]; if (entry != null && entry.equalsKey(hashCode, key)) { return true; } } return false; } } /** * Called under lock. */ void clear() { Arrays.fill(contents, null); // (Entries might be reused on iteration.) size = 0; } } static class LHMIterator implements Iterator> { private final HashMapLOW map; private LHMBucket[] buckets; /** Next index to check. */ private int bucketsIndex = 0; private LHMEntry[] currentBucket = null; /** Next index to check. */ private int currentBucketIndex = 0; private LHMEntry currentEntry = null; private K lastReturnedKey = null; LHMIterator(HashMapLOW map, LHMBucket[] buckets) { this.map = map; this.buckets = buckets; // (Lazily advance.) } /** * Advance internal state (generic/root). Set currentEntry or reset * buckets to null. */ private void advance() { currentEntry = null; if (buckets == null || currentBucket != null && advanceBucket()) { return; } for (int i = bucketsIndex; i < buckets.length; i++) { final LHMBucket bucket = buckets[i]; if (bucket != null) { this.currentBucket = bucket.contents; // Index should already be 0. if (advanceBucket()) { this.bucketsIndex = i + 1; // Next one. return; } } } // No remaining entries, finished. buckets = null; } /** * Advance within currentBucket. Reset if nothing found. * @return true if something was found. */ private boolean advanceBucket() { // First attempt to advance within first bucket. for (int i = currentBucketIndex; i < currentBucket.length; i++) { final LHMEntry entry = currentBucket[i]; if (entry != null) { currentBucketIndex = i + 1; currentEntry = entry; return true; } } // Nothing found, reset. currentBucket = null; currentBucketIndex = 0; return false; } @Override public boolean hasNext() { if (currentEntry != null) { return true; } else if (buckets == null) { return false; } else { advance(); return currentEntry != null; } } @Override public Entry next() { // Lazily advance. if (currentEntry == null) { advance(); if (currentEntry == null) { buckets = null; throw new NoSuchElementException(); } } final Entry entry = currentEntry; lastReturnedKey = entry.getKey(); currentEntry = null; return entry; } @Override public void remove() { if (lastReturnedKey == null) { throw new IllegalStateException(); } map.remove(lastReturnedKey); // TODO: CAN NOT WORK, NEED INVALIDATE ENTRY OTHERWISE lastReturnedKey = null; } } static class LHMIterable implements Iterable> { private final Iterator> iterator; LHMIterable(Iterator> iterator) { this.iterator = iterator; } @Override public Iterator> iterator() { return iterator; } } /////////////////////// // Instance members /////////////////////// private final Lock lock; /** Intended/expected size. */ private final int targetSize; /** Lazily filled with objects (null iff empty). */ private LHMBucket[] buckets; private int size = 0; private float loadFactor = 0.75f; // TODO: Configurable: loadFactor // TODO: Configurable: initial size and resize multiplier for Buckets. // TODO: Configurable: allow shrink. /** * Initialize with a ReentrantLock. * @param targetSize * Expected (average) number of elements in the map. */ public HashMapLOW(int targetSize) { this(new ReentrantLock(), targetSize); } /** * Initialize with a certain lock. * @param lock * @param targetSize */ public HashMapLOW(Lock lock, int targetSize) { this.lock = lock; this.targetSize = targetSize; buckets = newBuckets(targetSize); } /** * New buckets array for the given number of items. * * @param size * @return A new array to hold the given number of elements (size), using * internal settings. */ @SuppressWarnings("unchecked") private LHMBucket[] newBuckets(int size) { return (LHMBucket[]) new LHMBucket[Math.max((int) ((1f / loadFactor) * (float) size), targetSize)]; } /** * Resize according to the number of elements. Called under lock. */ private void resize() { final LHMBucket[] newBuckets = newBuckets(size); // Hold current number of elements. final int newLength = newBuckets.length; // Entries are reused, but not buckets (buckets would break iteration). for (int index = 0; index < buckets.length; index++) { final LHMBucket bucket = buckets[index]; if (bucket != null && bucket.size > 0) { for (int j = 0; j < bucket.contents.length; j++) { final LHMEntry entry = bucket.contents[j]; if (entry != null) { final int newIndex = getBucketIndex(entry.hashCode, newLength); LHMBucket newBucket = newBuckets[newIndex]; if (newBucket == null) { newBucket = new LHMBucket(); newBuckets[newIndex] = newBucket; } newBucket.addEntry(entry); } } } } buckets = newBuckets; } /** * Not under Lock. * @return */ public int size() { return size; } /** * Not under lock. * @return */ public boolean isEmpty() { return size == 0; } /** * Clear the map, detaching from iteration by unlinking storage containers. */ public void clear() { lock.lock(); buckets = newBuckets(targetSize); size = 0; lock.unlock(); } /** * Immediate put, under lock. * * @param key * @param value * @return */ public V put(final K key, final V value) { return put(key, value, false); } /** * Immediate put, only if there is no value or a null value set for the key, * under lock. * * @param key * @param value * @return */ public V putIfAbsent(final K key, final V value) { return put(key, value, true); } /** * * @param key * @param value * @param ifAbsent * If true, an existing non-null (!) value will not be * overridden. * @return */ private final V put(final K key, final V value, final boolean ifAbsent) { final int hashCode = getHashCode(key); lock.lock(); final int index = getBucketIndex(hashCode, buckets.length); LHMBucket bucket = buckets[index]; if (bucket == null) { bucket = new LHMBucket(); buckets[index] = bucket; } V oldValue = bucket.put(hashCode, key, value, ifAbsent); if (oldValue == null) { size ++; if (size > (int) (loadFactor * (float) buckets.length)) { resize(); } } lock.unlock(); return oldValue; } /** * Immediate remove, under lock. * * @param key * @return */ public V remove(final K key) { final int hashCode = getHashCode(key); lock.lock(); final V value = removeUnderLock(hashCode, key); // TODO: Shrink, if necessary. lock.unlock(); return value; } /** * Remove a value for a given key. Called under lock. Not intended to * shrink, due to being called on bulk removal. * * @param hashCode * @param key * @return */ private V removeUnderLock(final int hashCode, final K key) { final int index = getBucketIndex(hashCode, buckets.length); final LHMBucket bucket = buckets[index]; if (bucket == null || bucket.size == 0) { return null; } else { final V value = bucket.remove(hashCode, key); if (value != null) { size --; } return value; } } /** * Remove all given keys, using minimal locking. * * @param keys */ public void remove(final Collection keys) { lock.lock(); for (final K key : keys) { final int hashCode = getHashCode(key); removeUnderLock(hashCode, key); } lock.unlock(); } /** * Retrieve a value. Does not use locking. * * @param key * The stored value for the given key. Returns null if no value * is stored. */ public V get(final K key) { final int hashCode = getHashCode(key); final LHMBucket[] buckets = this.buckets; final LHMBucket bucket = buckets[getBucketIndex(hashCode, buckets.length)]; if (bucket == null || bucket.size == 0) { return null; } else { return bucket.get(hashCode, key); } } /** * Retrieve a value for a given key, or null if not existent. This method * uses locking. * * @param key */ public V getLocked(final K key) { lock.lock(); final V value = get(key); lock.unlock(); return value; } /** * Test if a mapping for this key exists. Accurate if key is null. Does not * use locking. * * @param key * @return */ public boolean containsKey(final K key) { final int hashCode = getHashCode(key); final LHMBucket[] buckets = this.buckets; final LHMBucket bucket = buckets[getBucketIndex(hashCode, buckets.length)]; if (bucket == null || bucket.size == 0) { return false; } else { return bucket.containsKey(hashCode, key); } } /** * Test if a mapping for this key exists. Accurate if key is null. This does * use locking. * * @param key * @return */ public boolean containsKeyLocked(final K key) { lock.lock(); final boolean res = containsKey(key); lock.unlock(); return res; } /** * Get an iterator reflecting this 'stage of resetting'. During iteration, * entries may get removed or added, values changed. Concurrent modification * will not let the iteration fail. * * @return */ public Iterator> iterator() { return size == 0 ? new LHMIterator(null, null) : new LHMIterator(this, buckets); } /** * Get an Iterable containing the same iterator, as is returned by * iterator(). See: {@link #iterator()} * * @return */ public Iterable> iterable() { return new LHMIterable(iterator()); } /** * Get all keys as a LinkedHashSet fit for iteration. The returned set is a * new instance, so changes don't affect the original HashMapLOW instance. * * @return */ public Collection getKeys() { final Set out = new LinkedHashSet(); final Iterator> it = iterator(); while (it.hasNext()) { out.add(it.next().getKey()); } return out; } }