Files
primitive-collections/primitiveCollections/src/main/java/org/lucares/collections/LongLongHashMap.java
Andreas Huber 062d63ca02 add maxCapacity to LongLongHashMap
This allows us to define an upper limit for the memory usage.
2021-04-16 17:52:15 +02:00

513 lines
13 KiB
Java

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.<p>
* This restricts the maximum memory used by this map. The memory consumption can be twice as much during grow or shrink phases.
* <p>
* Note that the performance can suffer if the map contains more keys than capacity time loadFactor.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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));
}
}