package org.lucares.collections; import java.util.Arrays; /** * A hash map where key and value are primitive longs. * * @see LongObjHashMap */ public class LongLongHashMap { // There is no equivalent to null for primitive values. Therefore we have to add // special handling for one long value. Otherwise we couldn't tell if a key is // in the map or not. We chose 0L, because LongList is initially all 0L. static final long NULL_KEY = 0L; // Needed when checking for the existence of a key. Without it we would be forced to // iterate over all keys. This is caused by the fact that we search for the next free // slot when adding new keys. // We rely on the fact that the value is -1! static final long REMOVED_KEY = -1L; private static final long EMPTY_SLOT = 0L; /** * The maximum size of an array. */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private final double fillFactor; private long[] keys; private long[] values; private int size = 0; /** * sentinel for the key {@value #NULL_KEY} ({@link #NULL_KEY}). * If this field is not null, then the map contains the key {@value #NULL_KEY} */ private Long zeroValue = null; /** * sentinel for the key {@value #REMOVED_KEY} ({@link #REMOVED_KEY}). * If this field is not null, then the map contains the key {@value #REMOVED_KEY} */ private Long removedKeyValue = null; private int maxCapacity = MAX_ARRAY_SIZE; /** * Create a new {@link LongLongHashMap} with the given initial capacity and load * factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor between 0 and 1 */ public LongLongHashMap(final int initialCapacity, final double loadFactor) { if (initialCapacity < 0) { throw new IllegalArgumentException("initial capacity must be non-negative"); } if (initialCapacity > MAX_ARRAY_SIZE) { throw new IllegalArgumentException("initial capacity must be smaller or equal to " + MAX_ARRAY_SIZE); } if (loadFactor <= 0 || Double.isNaN(loadFactor) || loadFactor >= 1.0) { throw new IllegalArgumentException("Illegal load factor, must be between 0 and 1: " + loadFactor); } this.fillFactor = loadFactor; keys = new long[initialCapacity]; values = new long[initialCapacity]; } /** * Create a new {@link LongLongHashMap} with initial capacity 8 and load factor * 0.75. */ public LongLongHashMap() { this(8, 0.75); } /** * Sets the maximum capacity.

* This restricts the maximum memory used by this map. The memory consumption can be twice as much during grow or shrink phases. *

* Note that the performance can suffer if the map contains more keys than capacity time loadFactor. *

* Note an automatic {@link #rehash()} is triggered if the new maxCapacity is smaller than the current capacity. * But there is not automatic rehash when the new maxCapacity is greater than the current capacity. * * @param maxCapacity new maximum capacity * @throws IllegalArgumentException if {@code maxCapacity} is smaller than {@link #size()} */ public void setMaxCapacity(int maxCapacity) { if (maxCapacity < size) { throw new IllegalArgumentException("maxCapacity must equal or larger than current size of the map"); } this.maxCapacity = maxCapacity; if (maxCapacity < keys.length) { rehash(maxCapacity); } } /** * The number of entries in this map. * * @return the size */ public int size() { return size; } @Override public String toString() { StringBuilder s = new StringBuilder(); if (zeroValue != null) { s.append(NULL_KEY); s.append("="); s.append(zeroValue); } if (removedKeyValue != null) { if (s.length() > 0) { s.append(", "); } s.append(REMOVED_KEY); s.append("="); s.append(removedKeyValue); } int values = 0; for (int i = 0; i < keys.length; i++) { if (keys[i] != EMPTY_SLOT && keys[i] != REMOVED_KEY) { if (s.length() > 0) { s.append(", "); } s.append(keys[i]); s.append("="); s.append(this.values[i]); values++; if (values > 10) { s.append(", ..."); break; } } } return s.toString(); } /** * The capacity of this map. * * @return the capacity */ int getCapacity() { return keys.length; } /** * Add the given key and value to the map. * * @param key the key * @param value the value * @throws IllegalStateException if the map is full, see {@link #setMaxCapacity(int)} */ public void put(final long key, final long value) { if (size == maxCapacity) { throw new IllegalStateException("map is full"); } if (key == NULL_KEY) { size += zeroValue == null ? 1 : 0; zeroValue = value; return; } if (key == REMOVED_KEY) { size += removedKeyValue == null ? 1 : 0; removedKeyValue = value; return; } if ((keys.length * fillFactor) < size) { growAndRehash(); } final boolean added = putInternal(key, value); if (added) { size++; } } private boolean putInternal(final long key, final long value) { final int searchStart = spread(key); int currentPosition = searchStart; do { // found a free place, insert the value if (keys[currentPosition] == EMPTY_SLOT) { keys[currentPosition] = key; values[currentPosition] = value; return true; } // value exists, update it if (keys[currentPosition] == key) { keys[currentPosition] = key; values[currentPosition] = value; return false; } currentPosition = (currentPosition + 1) % keys.length; } while (currentPosition != searchStart); // Can happen when all slots where occupied at some time in the past. // Easy to reproduce by adding and immediately removing all keys from 1 to n. // All slots will be marked with REMOVED_KEY. // We fix this by calling rehash(), which will effectively replace all REMOVED_KEY // with EMPTY_SLOT. rehash(); return putInternal(key, value); } /** * Returns the value for the given key if it exists or {@code defaultValue} if it does not exist. * * @param key the key * @param defaultValue the value to return if the given key does not exist * @return the value if it exists, or defaultValue */ public long get(final long key, long defaultValue) { if (key == NULL_KEY) { if (zeroValue != null) { return zeroValue; } return defaultValue; } if (key == REMOVED_KEY) { if (removedKeyValue != null) { return removedKeyValue; } return defaultValue; } final int searchStart = spread(key); int currentPosition = searchStart; do { if (keys[currentPosition] == key) { return values[currentPosition]; } else if (keys[currentPosition] == EMPTY_SLOT) { return defaultValue; } currentPosition = (currentPosition + 1) % keys.length; } while (currentPosition != searchStart); return defaultValue;// should never be reached unless the map is full at 100%, which should be impossible with loadFactor < 1.0 } /** * Check if the map contains the given key. * * @param key the key * @return true iff the map contains the key */ public boolean containsKey(final long key) { if (key == NULL_KEY) { return zeroValue != null; } if (key == REMOVED_KEY) { return removedKeyValue != null; } final int searchStart = spread(key); int currentPosition = searchStart; do { if (keys[currentPosition] == key) { return true; } else if (keys[currentPosition] == EMPTY_SLOT) { return false; } currentPosition = (currentPosition + 1) % keys.length; } while (currentPosition != searchStart); return false;// should never be reached unless the map is full at 100%, which should be impossible with loadFactor < 1.0 } /** * Remove the given key and its value from the map. * * @param key the key */ public void remove(final long key) { if (key == NULL_KEY) { size -= zeroValue != null ? 1 : 0; zeroValue = null; return; } if (key == REMOVED_KEY) { size -= removedKeyValue != null ? 1 : 0; removedKeyValue = null; return; } final int searchStart = spread(key); int currentPosition = searchStart; do { if (keys[currentPosition] == key) { keys[currentPosition] = REMOVED_KEY; size--; return; }else if (keys[currentPosition] == EMPTY_SLOT) { // key didn't exists return; } currentPosition = (currentPosition + 1) % keys.length; } while (currentPosition != searchStart); // run around should never happen unless the map is full at 100% } /** * Computes a mapping for the given key and its current value. *

* The mapping for given key is updated by calling {@code function} with the old * value. The return value will be set as new value. If the map does not contain * a mapping for the key, then {@code function} is called with * {@code initialValueIfAbsent}. * * @param key the key * @param initialValueIfAbsent value used if there is no current mapping for the * key * @param function called to update an existing value */ public void compute(final long key, final long initialValueIfAbsent, final BiLongFunction function) { if (key == NULL_KEY) { if (zeroValue != null) { zeroValue = function.apply(NULL_KEY,zeroValue); return; } zeroValue = function.apply(NULL_KEY,initialValueIfAbsent); return; } if (key == REMOVED_KEY) { if (removedKeyValue != null) { removedKeyValue = function.apply(REMOVED_KEY, removedKeyValue); return; } removedKeyValue = function.apply(REMOVED_KEY, initialValueIfAbsent); return; } final int searchStart = spread(key); int currentPosition = searchStart; do { if (keys[currentPosition] == key) { final long updatedValue = function.apply(key, values[currentPosition]); values[currentPosition] = updatedValue; return; } else if (keys[currentPosition] == EMPTY_SLOT) { // key not found break; } currentPosition = (currentPosition + 1) % keys.length; } while (currentPosition != searchStart); // key not found -> add it final long newZeroValue = function.apply(key, initialValueIfAbsent); put(key, newZeroValue); } /** * Calls the {@link LongLongConsumer#accept(long, long)} method for all entries * in this map. The order is based on the hash value and is therefore not * deterministic. Don't rely on the order! * * @param consumer the consumer */ public void forEach(final LongLongConsumer consumer) { if (zeroValue != null) { consumer.accept(NULL_KEY, zeroValue); } if (removedKeyValue != null) { consumer.accept(REMOVED_KEY, removedKeyValue); } for (int i = 0; i < keys.length; i++) { if (keys[i] != EMPTY_SLOT && keys[i] != REMOVED_KEY) { consumer.accept(keys[i], values[i]); } } } /** * Calls the {@link LongLongConsumer#accept(long, long)} method for all entries * in this map. This method iterates over the keys in ascending order. *

* Note: this method is slower than {@link #forEach(LongLongConsumer)}. * * @param consumer the consumer */ public void forEachOrdered(final LongLongConsumer consumer) { final long[] sortedKeys = Arrays.copyOf(keys, keys.length); Arrays.parallelSort(sortedKeys); // handle negative keys for (int i = 0; i < sortedKeys.length; i++) { final long key = sortedKeys[i]; if (key < REMOVED_KEY) { consumer.accept(key, get(key, 0)); // the default value of 'get' will not be used, because the key exists } else { break; } } // handle the special keys if (removedKeyValue != null) { consumer.accept(REMOVED_KEY, removedKeyValue); } if (zeroValue != null) { consumer.accept(NULL_KEY, zeroValue); } // handle positive keys final int posFirstKey = findPosOfFirstPositiveKey(sortedKeys); if (posFirstKey < 0) { return; } for (int i = posFirstKey; i < sortedKeys.length; i++) { final long key = sortedKeys[i]; consumer.accept(key, get(key, 0)); // the default value of 'get' will not be used, because the key exists } } static int findPosOfFirstPositiveKey(final long[] sortedKeys) { if (sortedKeys.length == 0) { return -1; } if (sortedKeys.length == 1) { return sortedKeys[0] > EMPTY_SLOT ? 0 : -1; } int low = 0; int high = sortedKeys.length - 1; int pos = -1; while (low <= high) { pos = (low + high) / 2; if (sortedKeys[pos] <= EMPTY_SLOT) { low = pos + 1; } else { high = pos - 1; } } if (low < sortedKeys.length && sortedKeys[low] <= EMPTY_SLOT) { low++; } return low < sortedKeys.length && sortedKeys[low] > EMPTY_SLOT ? low : -1; } /** * Rehashes all elements of this map. *

* This is a maintenance operation that should be executed periodically after removing elements. */ public void rehash() { rehash(keys.length); } private void growAndRehash() { final int newSize = Math.min(keys.length * 2, maxCapacity); if(newSize != keys.length) { rehash(newSize); } } private void rehash(int newSize) { final long[] oldKeys = keys; final long[] oldValues = values; keys = new long[newSize]; values = new long[newSize]; for (int i = 0; i < oldKeys.length; i++) { final long key = oldKeys[i]; if (key != EMPTY_SLOT && key != REMOVED_KEY) { final long value = oldValues[i]; putInternal(key, value); } } } // visible for test int spread(final long key) { return hash(key) % keys.length; } private int hash(final long l) { return Math.abs(Long.hashCode(l)); } }