diff --git a/agrona/src/main/java/org/agrona/collections/BiLong2NullableObjectMap.java b/agrona/src/main/java/org/agrona/collections/BiLong2NullableObjectMap.java new file mode 100644 index 000000000..d9bbd855d --- /dev/null +++ b/agrona/src/main/java/org/agrona/collections/BiLong2NullableObjectMap.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2023 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.agrona.collections; + +/** + * Variation of {@link BiLong2ObjectMap} that allows {@code null} values. + * + * @param type of values stored in the {@link java.util.Map} + */ +public class BiLong2NullableObjectMap extends BiLong2ObjectMap +{ + /** + * Constructs map with default settings. + */ + public BiLong2NullableObjectMap() + { + super(); + } + + /** + * Constructs map with given initial capacity and load factory and enables caching of iterators. + * + * @param initialCapacity for the backing array. + * @param loadFactor limit for resizing on puts. + */ + public BiLong2NullableObjectMap(final int initialCapacity, final float loadFactor) + { + super(initialCapacity, loadFactor); + } + + protected Object mapNullValue(final Object value) + { + return null == value ? NullReference.INSTANCE : value; + } + + @SuppressWarnings("unchecked") + protected V unmapNullValue(final Object value) + { + return NullReference.INSTANCE == value ? null : (V)value; + } +} diff --git a/agrona/src/main/java/org/agrona/collections/BiLong2ObjectMap.java b/agrona/src/main/java/org/agrona/collections/BiLong2ObjectMap.java new file mode 100644 index 000000000..8df11b4ed --- /dev/null +++ b/agrona/src/main/java/org/agrona/collections/BiLong2ObjectMap.java @@ -0,0 +1,944 @@ +/* + * Copyright 2014-2023 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.agrona.collections; + +import static java.util.Objects.requireNonNull; +import static org.agrona.BitUtil.findNextPositivePowerOfTwo; +import static org.agrona.collections.CollectionUtil.validateLoadFactor; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + + +/** + * Map that takes a two part `long` key and associates it with an object. + * + * @param type of the object stored in the map. + */ +public class BiLong2ObjectMap +{ + /** + * Handler for a map entry + * + * @param type of the value + */ + public interface EntryConsumer + { + /** + * A map entry + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param value for the entry + */ + void accept(long keyPartA, long keyPartB, V value); + } + + /** + * Creates a new value based upon keys. + * + * @param type of the value. + */ + public interface EntryFunction + { + /** + * A map entry. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @return value for the entry + */ + V apply(long keyPartA, long keyPartB); + } + + /** + * Creates a new value based upon keys. + * + * @param type of the value. + */ + public interface EntryRemap + { + /** + * A map entry. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param oldValue to be remapped + * @return value for the entry + */ + V1 apply(long keyPartA, long keyPartB, V oldValue); + } + + private static final int MIN_CAPACITY = 8; + + private final float loadFactor; + private int resizeThreshold; + private int size; + + private long[] keys; + private Object[] values; + + private static long keysA(final long[] keys, final int index) + { + return keys[index << 1]; + } + + private static long keysB(final long[] keys, final int index) + { + return keys[(index << 1) + 1]; + } + + private static void setKey(final long[] keys, final int index, final long keyPartA, final long keyPartB) + { + keys[index << 1] = keyPartA; + keys[(index << 1) + 1] = keyPartB; + } + + /** + * Construct an empty map. + */ + public BiLong2ObjectMap() + { + this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR); + } + + /** + * Construct a map that sets it initial capacity and load factor. + * + * @param initialCapacity for the underlying hash map + * @param loadFactor for the underlying hash map + */ + public BiLong2ObjectMap(final int initialCapacity, final float loadFactor) + { + validateLoadFactor(loadFactor); + + this.loadFactor = loadFactor; + final int capacity = findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity)); + this.resizeThreshold = (int)((float)capacity * loadFactor); + + final int keysCapacity = capacity << 1; + if (keysCapacity < 0) + { + throw new IllegalArgumentException("Capacity overflow at initialCapacity=" + initialCapacity); + } + this.keys = new long[keysCapacity]; + this.values = new Object[capacity]; + } + + /** + * Get the total capacity for the map to which the load factor with be a fraction of. + * + * @return the total capacity for the map. + */ + public int capacity() + { + return values.length; + } + + /** + * Get the load factor beyond which the map will increase size. + * + * @return load factor for when the map should increase size. + */ + public float loadFactor() + { + return loadFactor; + } + + /** + * Get the actual threshold which when reached the map will resize. + * This is a function of the current capacity and load factor. + * + * @return the threshold when the map will resize. + */ + public int resizeThreshold() + { + return resizeThreshold; + } + + /** + * Clear out the map of all entries. + * + * @see Map#clear() + */ + public void clear() + { + if (size > 0) + { + Arrays.fill(values, null); + size = 0; + } + } + + /** + * Compact the backing arrays by rehashing with a capacity just larger than current size + * and giving consideration to the load factor. + */ + public void compact() + { + final int idealCapacity = (int)Math.round(size() * (1.0 / loadFactor)); + rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); + } + + /** + * Put a value into the map. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param value to put into the map + * @return the previous value if found otherwise null + * @see Map#put(Object, Object) + */ + @SuppressWarnings("unchecked") + public V put(final long keyPartA, final long keyPartB, final V value) + { + final V val = (V)mapNullValue(value); + requireNonNull(val, "value cannot be null"); + + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object oldValue; + while (null != (oldValue = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + break; + } + + index = ++index & mask; + } + + if (null == oldValue) + { + ++size; + setKey(keys, index, keyPartA, keyPartB); + } + + values[index] = val; + + if (size > resizeThreshold) + { + increaseCapacity(); + } + + return unmapNullValue(oldValue); + } + + /** + * Interceptor for masking null values. + * + * @param value value to mask. + * @return masked value. + */ + protected Object mapNullValue(final Object value) + { + return value; + } + + /** + * Interceptor for unmasking null values. + * + * @param value value to unmask. + * @return unmasked value. + */ + @SuppressWarnings("unchecked") + protected V unmapNullValue(final Object value) + { + return (V)value; + } + + @SuppressWarnings("unchecked") + private V getMapping(final long keyPartA, final long keyPartB) + { + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object value; + while (null != (value = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + break; + } + + index = ++index & mask; + } + + return (V)value; + } + + /** + * Retrieve a value from the map. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @return value matching the key if found or null if not found. + * @see Map#get(Object) + */ + public V get(final long keyPartA, final long keyPartB) + { + return unmapNullValue(getMapping(keyPartA, keyPartB)); + } + + /** + * Retrieve a value from the map or defaultValue if this map contains not mapping for the key. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param defaultValue the default mapping of the key + * @return value matching the key if found or defaultValue if not found. + * @see java.util.Map#getOrDefault(Object, Object) + */ + public V getOrDefault(final long keyPartA, final long keyPartB, final V defaultValue) + { + final V val = getMapping(keyPartA, keyPartB); + return unmapNullValue(null != val ? val : defaultValue); + } + + /** + * Returns true if this map contains a mapping for the specified key. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @return true if this map contains a mapping for the specified key + * @see java.util.Map#containsKey(Object) + */ + public boolean containsKey(final long keyPartA, final long keyPartB) + { + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + boolean found = false; + while (null != values[index]) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + found = true; + break; + } + + index = ++index & mask; + } + + return found; + } + + /** + * Remove a value from the map and return the value. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @return the previous value if found otherwise null + * @see Map#remove(Object) + */ + @SuppressWarnings("unchecked") + public V remove(final long keyPartA, final long keyPartB) + { + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object value; + while (null != (value = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + values[index] = null; + --size; + + compactChain(index); + break; + } + + index = ++index & mask; + } + + return (V)value; + } + + + /** + * If the specified key is not already associated with a value (or is mapped + * to {@code null}), attempts to compute its value using the given mapping + * function and enters it into this map unless {@code null}. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param mappingFunction creates values based upon keys if the key pair is missing + * @return the newly created or stored value. + * @see Map#computeIfAbsent(Object, Function) + */ + public V computeIfAbsent(final long keyPartA, final long keyPartB, final EntryFunction mappingFunction) + { + requireNonNull(mappingFunction); + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object mappedValue; + while (null != (mappedValue = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + break; + } + + index = ++index & mask; + } + + V value = unmapNullValue(mappedValue); + + if (null == value && (value = mappingFunction.apply(keyPartA, keyPartB)) != null) + { + values[index] = value; + if (null == mappedValue) + { + setKey(keys, index, keyPartA, keyPartB); + if (++size > resizeThreshold) + { + increaseCapacity(); + } + } + } + + return value; + } + + /** + * If the value for the specified key is present and non-null, attempts to compute a new mapping given the key and + * its current mapped value. + *

+ * If the function returns null, the mapping is removed. If the function itself throws an (unchecked) exception, + * the exception is rethrown, and the current mapping is left unchanged. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param remappingFunction the function to compute a value + * @return the new value associated with the specified key, or null if none + * @see Map#computeIfPresent(Object, BiFunction) + */ + public V computeIfPresent( + final long keyPartA, + final long keyPartB, + final EntryRemap remappingFunction) + { + requireNonNull(remappingFunction); + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object mappedValue; + while (null != (mappedValue = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + break; + } + + index = ++index & mask; + } + + V value = unmapNullValue(mappedValue); + + if (null != value) + { + value = remappingFunction.apply(keyPartA, keyPartB, value); + values[index] = value; + if (null == value) + { + --size; + compactChain(index); + } + } + + return value; + } + + /** + * Attempts to compute a mapping for the specified key and its current mapped value (or null if there is no current + * mapping). + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param remappingFunction the function to compute a value + * @return the new value associated with the specified key, or null if none + * @see Map#compute(Object, BiFunction) + */ + public V compute( + final long keyPartA, + final long keyPartB, + final EntryRemap remappingFunction) + { + requireNonNull(remappingFunction); + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object mappedvalue; + while (null != (mappedvalue = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + break; + } + + index = ++index & mask; + } + + final V newValue = remappingFunction.apply(keyPartA, keyPartB, unmapNullValue(mappedvalue)); + if (null != newValue) + { + values[index] = newValue; + if (null == mappedvalue) + { + setKey(keys, index, keyPartA, keyPartB); + if (++size > resizeThreshold) + { + increaseCapacity(); + } + } + } + else if (null != mappedvalue) + { + values[index] = null; + size--; + compactChain(index); + } + + return newValue; + } + + /** + * If the specified key is not already associated with a value or is associated with null, associates it with the + * given non-null value. Otherwise, replaces the associated value with the results of the given remapping function, + * or removes if the result is null. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param value the non-null value to be merged with the existing value associated with the key or, if + * no existing value or a null value is associated with the key, to be associated with the + * key + * @param remappingFunction the function to recompute a value if present + * @return the new value associated with the specified key, or null if no value is associated with the key + * @see Map#merge(Object, Object, BiFunction) + */ + public V merge( + final long keyPartA, + final long keyPartB, + final V value, + final BiFunction remappingFunction) + { + requireNonNull(value); + requireNonNull(remappingFunction); + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object mappedvalue; + while (null != (mappedvalue = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + break; + } + + index = ++index & mask; + } + + final V oldValue = unmapNullValue(mappedvalue); + final V newValue = null == oldValue ? value : remappingFunction.apply(oldValue, value); + + if (null != newValue) + { + values[index] = newValue; + if (null == mappedvalue) + { + setKey(keys, index, keyPartA, keyPartB); + if (++size > resizeThreshold) + { + increaseCapacity(); + } + } + } + else if (null != mappedvalue) + { + values[index] = null; + size--; + compactChain(index); + } + + return newValue; + } + + /** + * Iterate over the contents of the map + * + * @param consumer to apply to each value in the map + */ + @SuppressWarnings("unchecked") + public void forEach(final Consumer consumer) + { + int remaining = this.size; + final Object[] values = this.values; + + for (int i = 0, length = values.length; remaining > 0 && i < length; i++) + { + final Object value = values[i]; + if (null != value) + { + consumer.accept((V)value); + --remaining; + } + } + } + + /** + * Iterate over the contents of the map + * + * @param consumer to apply to each value in the map + */ + public void forEach(final EntryConsumer consumer) + { + int remaining = this.size; + final long[] keys = this.keys; + final Object[] values = this.values; + + for (int i = 0, length = values.length; remaining > 0 && i < length; i++) + { + final Object value = values[i]; + if (null != value) + { + final long keyPartA = keysA(keys, i); + final long keyPartB = keysB(keys, i); + + consumer.accept(keyPartA, keyPartB, unmapNullValue(value)); + --remaining; + } + } + } + + /** + * Replaces the entry for the specified key only if currently mapped to the specified value. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or null if there was no mapping for the key. + * (A null return can also indicate that the map previously associated null with the key, if the implementation + * supports null values.) + * @see Map#replace(Object, Object) + */ + @SuppressWarnings("unchecked") + public V replace(final long keyPartA, final long keyPartB, final V value) + { + final V val = (V)mapNullValue(value); + requireNonNull(val, "value cannot be null"); + + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object oldValue; + while (null != (oldValue = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + values[index] = val; + break; + } + + index = ++index & mask; + } + + return unmapNullValue(oldValue); + } + + /** + * Replaces the entry for the specified key only if currently mapped to the specified value. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param oldValue value expected to be associated with the specified key + * @param newValue to be associated with the specified key + * @return true if the value was replaced + */ + @SuppressWarnings("unchecked") + public boolean replace(final long keyPartA, final long keyPartB, final V oldValue, final V newValue) + { + final V val = (V)mapNullValue(newValue); + requireNonNull(val, "value cannot be null"); + + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object mappedValue; + while (null != (mappedValue = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + if (Objects.equals(unmapNullValue(mappedValue), oldValue)) + { + values[index] = val; + return true; + } + break; + } + + index = ++index & mask; + } + + return false; + } + + /** + * If the specified key is not already associated with a value (or is mapped to null) associates it with the given + * value and returns null, else returns the current value. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param value to put into the map + * @return the previous value if found otherwise null + */ + @SuppressWarnings("unchecked") + public V putIfAbsent(final long keyPartA, final long keyPartB, final V value) + { + final V val = (V)mapNullValue(value); + requireNonNull(val, "value cannot be null"); + + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object mappedValue; + while (null != (mappedValue = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + break; + } + + index = ++index & mask; + } + + final V oldValue = unmapNullValue(mappedValue); + if (null == oldValue) + { + if (null == mappedValue) + { + ++size; + setKey(keys, index, keyPartA, keyPartB); + } + + values[index] = val; + + if (size > resizeThreshold) + { + increaseCapacity(); + } + } + + return oldValue; + } + + /** + * Removes the entry for the specified key only if it is currently mapped to the specified value. + * + * @param keyPartA for the key + * @param keyPartB for the key + * @param value value expected to be associated with the specified key + * @return true if the value was removed + * @see Map#remove(Object, Object) + */ + public boolean remove(final long keyPartA, final long keyPartB, final V value) + { + final Object val = mapNullValue(value); + if (null != val) + { + final long[] keys = this.keys; + final Object[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(keyPartA, keyPartB, mask); + + Object mappedValue; + while (null != (mappedValue = values[index])) + { + if (keyPartA == keysA(keys, index) && keyPartB == keysB(keys, index)) + { + if (Objects.equals(unmapNullValue(mappedValue), value)) + { + values[index] = null; + --size; + + compactChain(index); + return true; + } + break; + } + + index = ++index & mask; + } + } + return false; + } + + /** + * Return the number of unique entries in the map. + * + * @return number of unique entries in the map. + */ + public int size() + { + return size; + } + + /** + * Is map empty or not. + * + * @return boolean indicating empty map or not + */ + public boolean isEmpty() + { + return 0 == size; + } + + /** + * {@inheritDoc} + */ + public String toString() + { + final StringBuilder sb = new StringBuilder(); + sb.append('{'); + + final long[] keys = this.keys; + final Object[] values = this.values; + for (int i = 0, size = values.length; i < size; i++) + { + final Object value = values[i]; + if (null != value) + { + final long keyPartA = keysA(keys, i); + final long keyPartB = keysB(keys, i); + + sb.append(keyPartA).append('_').append(keyPartB).append('=').append(value).append(", "); + } + } + + if (sb.length() > 1) + { + sb.setLength(sb.length() - 2); + } + + sb.append('}'); + + return sb.toString(); + } + + private void rehash(final int newCapacity) + { + final int mask = newCapacity - 1; + resizeThreshold = (int)(newCapacity * loadFactor); + + final long[] tempKeys = new long[newCapacity << 1]; + final Object[] tempValues = new Object[newCapacity]; + + final long[] keys = this.keys; + final Object[] values = this.values; + for (int i = 0, size = values.length; i < size; i++) + { + final Object value = values[i]; + if (null != value) + { + final long keyPartA = keysA(keys, i); + final long keyPartB = keysB(keys, i); + int newHash = Hashing.hash(keyPartA, keyPartB, mask); + + while (null != tempValues[newHash]) + { + newHash = ++newHash & mask; + } + + setKey(tempKeys, newHash, keyPartA, keyPartB); + tempValues[newHash] = value; + } + } + + this.keys = tempKeys; + this.values = tempValues; + } + + @SuppressWarnings("FinalParameters") + private void compactChain(int deleteIndex) + { + final int mask = values.length - 1; + int index = deleteIndex; + final long[] keys = this.keys; + final Object[] values = this.values; + while (true) + { + index = ++index & mask; + final Object value = values[index]; + if (null == value) + { + break; + } + + final long keyPartA = keysA(keys, index); + final long keyPartB = keysB(keys, index); + final int hash = Hashing.hash(keyPartA, keyPartB, mask); + + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) || + (hash <= deleteIndex && deleteIndex <= index)) + { + setKey(keys, deleteIndex, keyPartA, keyPartB); + values[deleteIndex] = value; + + values[index] = null; + deleteIndex = index; + } + } + } + + private void increaseCapacity() + { + final int newKeysCapacity = keys.length << 1; + if (newKeysCapacity < 0) + { + throw new IllegalStateException("max capacity reached at size=" + size); + } + // keys.length == values.length << 1 + rehash(keys.length); + } +} diff --git a/agrona/src/main/java/org/agrona/collections/Hashing.java b/agrona/src/main/java/org/agrona/collections/Hashing.java index b69ec5713..f0bb3a6d9 100644 --- a/agrona/src/main/java/org/agrona/collections/Hashing.java +++ b/agrona/src/main/java/org/agrona/collections/Hashing.java @@ -99,6 +99,20 @@ public static int hash(final long value, final int mask) return hash(value) & mask; } + /** + * Generate a hash for a pair of long values and apply a mask to get a remainder. + * + * @param valueA first long value to be hashed. + * @param valueB second long value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the values + */ + public static int hash(final long valueA, final long valueB, final int mask) + { + final int hash = 31 * Hashing.hash(valueA) + Hashing.hash(valueB); + return hash & mask; + } + /** * Generate an even hash for an int value and apply mask to get a remainder that will be even. * diff --git a/agrona/src/test/java/org/agrona/collections/BiLong2NullableObjectMapTest.java b/agrona/src/test/java/org/agrona/collections/BiLong2NullableObjectMapTest.java new file mode 100644 index 000000000..cfd23e6fc --- /dev/null +++ b/agrona/src/test/java/org/agrona/collections/BiLong2NullableObjectMapTest.java @@ -0,0 +1,504 @@ +/* + * Copyright 2014-2023 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.agrona.collections; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.stubbing.Answer; + +import java.util.function.BiFunction; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +class BiLong2NullableObjectMapTest +{ + @Test + void getOrDefaultShouldReturnDefaultValueIfNoMappingExistsForAGivenKey() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = -2; + final String defaultValue = "fallback"; + + assertEquals(defaultValue, map.getOrDefault(key, key, defaultValue)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = "abc") + void getOrDefaultShouldReturnExistingValueForTheGivenKey(final String value) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 121; + final String defaultValue = "default value"; + map.put(key, key, value); + + assertEquals(value, map.getOrDefault(key, key, defaultValue)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = "abc") + void replaceShouldReturnAnOldValueAfterReplacingAnExistingValue(final String value) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = Long.MIN_VALUE; + final String newValue = "new value"; + map.put(key, key, value); + + assertEquals(value, map.replace(key, key, newValue)); + + assertEquals(newValue, map.get(key, key)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = "xyz") + void replaceShouldReturnTrueAfterReplacingAnExistingValue(final String value) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = Long.MAX_VALUE; + final String newValue = "new value"; + map.put(key, key, value); + + assertTrue(map.replace(key, key, value, newValue)); + assertEquals(newValue, map.get(key, key)); + } + + @Test + void replaceShouldReplaceWithNullValue() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 0; + final String value = "change me"; + map.put(key, key, value); + + assertTrue(map.replace(key, key, value, null)); + assertEquals(1, map.size()); + assertNull(map.get(key, key)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"val 1", "你好"}) + void putIfAbsentShouldReturnAnExistingValueForAnExistingKey(final String value) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = -42; + final String newValue = " this is something new"; + map.put(key, key, value); + + assertEquals(value, map.putIfAbsent(key, key, newValue)); + } + + @Test + void putIfAbsentShouldReturnNullAfterReplacingExistingNullMapping() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 42; + final String newValue = " this is something new"; + map.put(key, key, null); + + assertNull(map.putIfAbsent(key, key, newValue)); + + assertEquals(newValue, map.get(key, key)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"val 1", "你好"}) + void putIfAbsentShouldReturnNullAfterPuttingANewValue(final String newValue) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 42; + map.put(3, 3, "three"); + + assertNull(map.putIfAbsent(key, key, newValue)); + + assertEquals(newValue, map.get(key, key)); + assertEquals("three", map.get(3, 3)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"val 1", "你好"}) + void removeReturnsTrueAfterRemovingTheKey(final String value) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 42; + map.put(3, 3, "three"); + map.put(key, key, value); + + assertTrue(map.remove(key, key, value)); + + assertEquals(1, map.size()); + assertEquals("three", map.get(3, 3)); + } + + @Test + void computeIfAbsentThrowsNullPointerExceptionIfMappingFunctionIsNull() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 2; + + assertThrows(NullPointerException.class, () -> map.computeIfAbsent(key, key, null)); + } + + @ParameterizedTest + @EmptySource + @ValueSource(strings = {"val 1", "你好"}) + @SuppressWarnings("unchecked") + void computeIfAbsentReturnsAnExistingValueWithoutInvokingTheMappingFunction(final String value) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 2; + final BiLong2ObjectMap.EntryFunction mappingFunction = mock(BiLong2ObjectMap.EntryFunction.class); + map.put(key, key, value); + + assertEquals(value, map.computeIfAbsent(key, key, mappingFunction)); + + assertEquals(value, map.get(key, key)); + verifyNoInteractions(mappingFunction); + } + + @Test + @SuppressWarnings("unchecked") + void computeIfAbsentReturnsNullIfMappingFunctionReturnsNull() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final BiLong2ObjectMap.EntryFunction mappingFunction = mock(BiLong2ObjectMap.EntryFunction.class); + final long key = 2; + + assertNull(map.computeIfAbsent(key, key, mappingFunction)); + + assertFalse(map.containsKey(key, key)); + verify(mappingFunction).apply(key, key); + verifyNoMoreInteractions(mappingFunction); + } + + @Test + @SuppressWarnings("unchecked") + void computeIfAbsentReturnsNewValueAfterCreatingAMapping() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 2; + final String value = "new value"; + final BiLong2ObjectMap.EntryFunction mappingFunction = mock(BiLong2ObjectMap.EntryFunction.class); + when(mappingFunction.apply(key, key)).thenReturn(value); + + assertEquals(value, map.computeIfAbsent(key, key, mappingFunction)); + + assertTrue(map.containsKey(key, key)); + assertEquals(value, map.get(key, key)); + verify(mappingFunction).apply(key, key); + verifyNoMoreInteractions(mappingFunction); + } + + @Test + @SuppressWarnings("unchecked") + void computeIfAbsentReturnsNewValueAfterReplacingNullMapping() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = -190; + map.put(key, key, null); + final String value = "new value"; + final BiLong2ObjectMap.EntryFunction mappingFunction = mock(BiLong2ObjectMap.EntryFunction.class); + when(mappingFunction.apply(key, key)).thenReturn(value); + + assertEquals(value, map.computeIfAbsent(key, key, mappingFunction)); + + assertTrue(map.containsKey(key, key)); + assertEquals(value, map.get(key, key)); + verify(mappingFunction).apply(key, key); + verifyNoMoreInteractions(mappingFunction); + } + + @Test + void computeIfPresentThrowsNullPointerExceptionIfRemappingFunctionIsNull() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 3; + + assertThrowsExactly(NullPointerException.class, () -> map.computeIfPresent(key, key, null)); + } + + @Test + @SuppressWarnings("unchecked") + void computeIfPresentReturnsNullForNonExistingKey() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final BiLong2ObjectMap.EntryRemap remappingFunction = mock(BiLong2ObjectMap.EntryRemap.class); + final long key = 3; + + assertNull(map.computeIfPresent(key, key, remappingFunction)); + + assertFalse(map.containsKey(key, key)); + verifyNoInteractions(remappingFunction); + } + + @Test + @SuppressWarnings("unchecked") + void computeIfPresentReturnsNullForIfKeyIsMappedToNull() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final BiLong2ObjectMap.EntryRemap remappingFunction = mock(BiLong2ObjectMap.EntryRemap.class); + final long key = 3; + map.put(key, key, null); + + assertNull(map.computeIfPresent(key, key, remappingFunction)); + + assertTrue(map.containsKey(key, key)); + assertNull(map.get(key, key)); + verifyNoInteractions(remappingFunction); + } + + @ParameterizedTest + @EmptySource + @ValueSource(strings = {"val 1", "你好"}) + void computeIfPresentReturnsNewValueAfterAnUpdate(final String oldValue) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 42; + map.put(key, key, oldValue); + map.put(5, 5, "five"); + final BiLong2ObjectMap.EntryRemap remappingFunction = (k1, k2, v) -> k1 + k2 + v; + final String expectedNewValue = key + key + oldValue; + + assertEquals(expectedNewValue, map.computeIfPresent(key, key, remappingFunction)); + + assertEquals(expectedNewValue, map.get(key, key)); + assertEquals("five", map.get(5, 5)); + } + + @ParameterizedTest + @EmptySource + @ValueSource(strings = {"val 1", "你好"}) + void computeIfPresentReturnsNullAfterRemovingAnExistingValue(final String value) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 42; + map.put(key, key, value); + map.put(5, 5, "five"); + @SuppressWarnings("unchecked") final BiLong2ObjectMap.EntryRemap remappingFunction = + mock(BiLong2ObjectMap.EntryRemap.class); + + assertNull(map.computeIfPresent(key, key, remappingFunction)); + + assertFalse(map.containsKey(key, key)); + assertEquals("five", map.get(5, 5)); + } + + + @Test + void computeThrowsNullPointerExceptionIfRemappingFunctionIsNull() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 3; + + assertThrowsExactly(NullPointerException.class, () -> map.compute(key, key, null)); + } + + @Test + @SuppressWarnings("unchecked") + void computeReturnsNullForUnExistingKey() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final BiLong2ObjectMap.EntryRemap remappingFunction = mock(BiLong2ObjectMap.EntryRemap.class); + final long key = 3; + map.put(5, 5, "five"); + + assertNull(map.compute(key, key, remappingFunction)); + + assertEquals("five", map.get(5, 5)); + assertFalse(map.containsKey(key, key)); + verify(remappingFunction).apply(key, key, null); + verifyNoMoreInteractions(remappingFunction); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"val 1", "你好"}) + void computeReturnsNullAfterRemovingAnExistingValue(final String value) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 42; + map.put(key, key, value); + map.put(5, 5, "five"); + final BiLong2ObjectMap.EntryRemap remappingFunction = (k1, k2, v) -> null; + + assertNull(map.compute(key, key, remappingFunction)); + + assertFalse(map.containsKey(key, key)); + assertEquals("five", map.get(5, 5)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"val 1", "你好"}) + void computeReturnsANewValueAfterReplacingAnExistingOne(final String oldValue) + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 42; + map.put(key, key, oldValue); + map.put(5, 5, "five"); + final String newValue = key + key + oldValue; + final BiLong2ObjectMap.EntryRemap remappingFunction = (k1, k2, v) -> + { + assertEquals(key, k1); + assertEquals(key, k2); + assertEquals(oldValue, v); + return k1 + k2 + v; + }; + + assertEquals(newValue, map.compute(key, key, remappingFunction)); + + assertEquals(newValue, map.get(key, key)); + assertEquals("five", map.get(5, 5)); + } + + @Test + void computeReturnsANewValueAfterCreatingANewMapping() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 42; + map.put(5, 5, "five"); + final String newValue = String.valueOf(System.currentTimeMillis()); + final BiLong2ObjectMap.EntryRemap remappingFunction = (k1, k2, v) -> newValue; + + assertEquals(newValue, map.compute(key, key, remappingFunction)); + + assertEquals(newValue, map.get(key, key)); + assertEquals("five", map.get(5, 5)); + } + + @Test + void mergeThrowsNullPointerExceptionIfValueIsNull() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 42; + final BiFunction remappingFunction = (oldValue, newValue) -> null; + + assertThrowsExactly(NullPointerException.class, () -> map.merge(key, key, null, remappingFunction)); + } + + @Test + void mergeThrowsNullPointerExceptionIfRemappingFunctionIsNull() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 8888; + final String value = "value"; + + assertThrowsExactly(NullPointerException.class, () -> map.merge(key, key, value, null)); + } + + @Test + @SuppressWarnings("unchecked") + void mergeShouldPutANewMappingForAnUnknownKeyWithoutCallingARemappingFunction() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 8888; + final String value = "value"; + final BiFunction remappingFunction = mock(BiFunction.class); + + assertEquals(value, map.merge(key, key, value, remappingFunction)); + + assertEquals(value, map.get(key, key)); + verifyNoInteractions(remappingFunction); + } + + @Test + @SuppressWarnings("unchecked") + void mergeShouldReplaceNullMappingWithAGivenValue() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 8888; + final String value = "value"; + final BiFunction remappingFunction = mock(BiFunction.class); + map.put(key, key, null); + + assertEquals(value, map.merge(key, key, value, remappingFunction)); + + assertEquals(value, map.get(key, key)); + verifyNoInteractions(remappingFunction); + } + + @Test + @SuppressWarnings("unchecked") + void mergeShouldReplaceExistingValueWithComputedValue() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + final long key = 8888; + final String value = "value"; + final String newValue = "NEW"; + final String computedValue = "value => NEW"; + final BiFunction remappingFunction = mock(BiFunction.class); + when(remappingFunction.apply(any(), any())).thenAnswer((Answer)invocation -> + { + final String oldVal = invocation.getArgument(0); + final String val = invocation.getArgument(1); + return oldVal + " => " + val; + }); + map.put(key, key, value); + + assertEquals(computedValue, map.merge(key, key, newValue, remappingFunction)); + + assertEquals(computedValue, map.get(key, key)); + verify(remappingFunction).apply(value, newValue); + verifyNoMoreInteractions(remappingFunction); + } + + @Test + void forEachIntShouldUnmapNullValuesBeforeInvokingTheAction() + { + final BiLong2NullableObjectMap map = new BiLong2NullableObjectMap<>(); + map.put(1, 1, "one"); + map.put(2, 2, null); + map.put(3, 3, "three"); + map.put(4, 4, null); + final MutableInteger count = new MutableInteger(); + final BiLong2ObjectMap.EntryConsumer action = (k1, k2, v) -> + { + count.increment(); + if ((k1 & 1) == 0) + { + assertNull(v); + } + else + { + assertNotNull(v); + } + }; + + map.forEach(action); + + assertEquals(4, count.get()); + assertEquals(4, map.size()); + } +} diff --git a/agrona/src/test/java/org/agrona/collections/BiLong2ObjectMapTest.java b/agrona/src/test/java/org/agrona/collections/BiLong2ObjectMapTest.java new file mode 100644 index 000000000..c687ded74 --- /dev/null +++ b/agrona/src/test/java/org/agrona/collections/BiLong2ObjectMapTest.java @@ -0,0 +1,474 @@ +/* + * Copyright 2014-2023 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.agrona.collections; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.either; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class BiLong2ObjectMapTest +{ + private final BiLong2ObjectMap map = new BiLong2ObjectMap<>(); + + @Test + void shouldInitialiseUnderlyingImplementation() + { + final int initialCapacity = 10; + final float loadFactor = 0.6f; + final BiLong2ObjectMap map = new BiLong2ObjectMap<>(initialCapacity, loadFactor); + + assertThat(map.capacity(), either(is(initialCapacity)).or(greaterThan(initialCapacity))); + assertThat(map.loadFactor(), is(loadFactor)); + } + + @Test + void shouldReportEmpty() + { + assertThat(map.isEmpty(), is(true)); + } + + @Test + void shouldPutItem() + { + final String testValue = "Test"; + final long keyPartA = 3; + final long keyPartB = 7; + + assertNull(map.put(keyPartA, keyPartB, testValue)); + assertThat(map.size(), is(1)); + } + + @Test + void shouldPutAndGetItem() + { + final String testValue = "Test"; + final long keyPartA = 3; + final long keyPartB = 7; + + assertNull(map.put(keyPartA, keyPartB, testValue)); + assertThat(map.get(keyPartA, keyPartB), is(testValue)); + } + + @Test + void shouldReturnNullWhenNotFoundItem() + { + final int keyPartA = 3; + final int keyPartB = 7; + + assertNull(map.get(keyPartA, keyPartB)); + } + + @Test + void shouldRemoveItem() + { + final String testValue = "Test"; + final int keyPartA = 3; + final int keyPartB = 7; + + map.put(keyPartA, keyPartB, testValue); + assertThat(map.remove(keyPartA, keyPartB), is(testValue)); + assertNull(map.get(keyPartA, keyPartB)); + } + + @Test + void shouldIterateValues() + { + final Set expectedSet = new HashSet<>(); + final int count = 7; + + for (int i = 0; i < count; i++) + { + final String value = String.valueOf(i); + expectedSet.add(value); + map.put(i, i + 97, value); + } + + final Set actualSet = new HashSet<>(); + + map.forEach(actualSet::add); + + assertThat(actualSet, equalTo(expectedSet)); + } + + @Test + void shouldIterateEntries() + { + final Set> expectedSet = new HashSet<>(); + final int count = 7; + + for (int i = 0; i < count; i++) + { + final String value = String.valueOf(i); + expectedSet.add(new EntryCapture<>(i, i + 97, value)); + map.put(i, i + 97, value); + } + + final Set> actualSet = new HashSet<>(); + + map.forEach((keyPartA, keyPartB, value) -> actualSet.add(new EntryCapture<>(keyPartA, keyPartB, value))); + + assertThat(actualSet, equalTo(expectedSet)); + } + + @Test + void shouldToString() + { + final int count = 7; + + for (int i = 0; i < count; i++) + { + final String value = String.valueOf(i); + map.put(i, i + 97, value); + } + + assertThat(map.toString(), is("{1_98=1, 0_97=0, 2_99=2, 4_101=4, 5_102=5, 3_100=3, 6_103=6}")); + } + + @Test + void shouldPutAndGetKeysOfNegativeValue() + { + map.put(721632679, 333118496, "a"); + assertThat(map.get(721632679, 333118496), is("a")); + + map.put(721632719, -659033725, "b"); + assertThat(map.get(721632719, -659033725), is("b")); + + map.put(721632767, -235401032, "c"); + assertThat(map.get(721632767, -235401032), is("c")); + + map.put(721632839, 1791470537, "d"); + assertThat(map.get(721632839, 1791470537), is("d")); + + map.put(721633069, -939458690, "e"); + assertThat(map.get(721633069, -939458690), is("e")); + + map.put(721633127, 1620485039, "f"); + assertThat(map.get(721633127, 1620485039), is("f")); + + map.put(721633163, -1503337805, "g"); + assertThat(map.get(721633163, -1503337805), is("g")); + + map.put(721633229, -2073657736, "h"); + assertThat(map.get(721633229, -2073657736), is("h")); + + map.put(721633255, -1278969172, "i"); + assertThat(map.get(721633255, -1278969172), is("i")); + + map.put(721633257, -1230662585, "j"); + assertThat(map.get(721633257, -1230662585), is("j")); + + map.put(721633319, -532637417, "k"); + assertThat(map.get(721633319, -532637417), is("k")); + } + + @Test + void shouldRejectNullValues() + { + assertThrows(NullPointerException.class, () -> map.put(1, 2, null)); + assertThrows(NullPointerException.class, () -> map.putIfAbsent(1, 2, null)); + } + + @Test + void shouldGrowAndCanCompact() + { + final Map reference = new HashMap<>(); + + for (int i = 0; i < 20000; i++) + { + final UUID key = UUID.randomUUID(); + final String value = "" + i; + final String other = "other" + i; + assertNull(map.put(key.getMostSignificantBits(), key.getLeastSignificantBits(), other)); + assertThat(map.containsKey(key.getMostSignificantBits(), key.getLeastSignificantBits()), is(true)); + assertThat(map.remove(key.getMostSignificantBits(), key.getLeastSignificantBits(), value), is(false)); + assertThat(map.remove(key.getMostSignificantBits(), key.getLeastSignificantBits(), other), is(true)); + assertNull(map.remove(key.getMostSignificantBits(), key.getLeastSignificantBits())); + assertThat(map.containsKey(key.getMostSignificantBits(), key.getLeastSignificantBits()), is(false)); + assertNull(map.putIfAbsent(key.getMostSignificantBits(), key.getLeastSignificantBits(), value)); + assertThat(map.putIfAbsent(key.getMostSignificantBits(), key.getLeastSignificantBits(), other), is(value)); + reference.put(key, value); + } + + assertThat(map.size(), is(reference.size())); + + final UUID notInMap = UUID.randomUUID(); + assertThat(map.containsKey(notInMap.getMostSignificantBits(), notInMap.getLeastSignificantBits()), is(false)); + assertNull(map.get(notInMap.getMostSignificantBits(), notInMap.getLeastSignificantBits())); + assertThat(map.getOrDefault(notInMap.getMostSignificantBits(), notInMap.getLeastSignificantBits(), "default"), + is("default")); + + for (final UUID key : reference.keySet()) + { + assertThat(map.containsKey(key.getMostSignificantBits(), key.getLeastSignificantBits()), is(true)); + assertThat(map.get(key.getMostSignificantBits(), key.getLeastSignificantBits()), is(reference.get(key))); + assertThat(map.getOrDefault(key.getMostSignificantBits(), key.getLeastSignificantBits(), "default"), + is(reference.get(key))); + } + + while (map.size() > 0) + { + final int toRemove = Math.max(1, map.size() / 2); + final List keysToRemove = reference.keySet().stream().limit(toRemove).collect(Collectors.toList()); + for (final UUID key : keysToRemove) + { + final String expected = reference.remove(key); + assertThat(expected, is(not(nullValue()))); + assertThat(map.remove(key.getMostSignificantBits(), key.getLeastSignificantBits()), is(expected)); + } + map.compact(); + } + + assertThat(map.isEmpty(), is(true)); + } + + private void testAgainstReference(final PutFunction putFunction, final RemoveFunction removeFunction) + { + final Map reference = new HashMap<>(); + + for (int i = 0; i < 20000; i++) + { + final UUID key = UUID.randomUUID(); + final long keyPartA = key.getMostSignificantBits(); + final long keyPartB = key.getLeastSignificantBits(); + final String value = "" + i; + final String other = "other" + i; + putFunction.putInMap(map, keyPartA, keyPartB, null, value); + assertThat(map.containsKey(key.getMostSignificantBits(), key.getLeastSignificantBits()), is(true)); + removeFunction.removeFromMap(map, keyPartA, keyPartB, value); + assertThat(map.containsKey(key.getMostSignificantBits(), key.getLeastSignificantBits()), is(false)); + putFunction.putInMap(map, keyPartA, keyPartB, null, other); + putFunction.putInMap(map, keyPartA, keyPartB, other, value); + reference.put(key, value); + } + + assertThat(map.size(), is(reference.size())); + + final UUID notInMap = UUID.randomUUID(); + assertThat(map.containsKey(notInMap.getMostSignificantBits(), notInMap.getLeastSignificantBits()), is(false)); + assertNull(map.get(notInMap.getMostSignificantBits(), notInMap.getLeastSignificantBits())); + assertThat(map.getOrDefault(notInMap.getMostSignificantBits(), notInMap.getLeastSignificantBits(), "default"), + is("default")); + + for (final UUID key : reference.keySet()) + { + final long keyPartA = key.getMostSignificantBits(); + final long keyPartB = key.getLeastSignificantBits(); + assertThat(map.containsKey(keyPartA, keyPartB), is(true)); + assertThat(map.get(keyPartA, keyPartB), is(reference.get(key))); + assertThat(map.getOrDefault(keyPartA, keyPartB, "default"), is(reference.get(key))); + } + } + + @Test + void shouldBlindReplace() + { + testAgainstReference( + (map, keyPartA, keyPartB, expectedCurrentValue, newValue) -> + { + if (expectedCurrentValue == null) + { + assertNull(map.put(keyPartA, keyPartB, "placeholder")); + assertThat(map.replace(keyPartA, keyPartB, newValue), is("placeholder")); + } + else + { + final String actualOldValue = map.replace(keyPartA, keyPartB, newValue); + assertThat(actualOldValue, is(expectedCurrentValue)); + } + }, + (map, keyPartA, keyPartB, expectedCurrentValue) -> + { + final String previousValue = map.remove(keyPartA, keyPartB); + assertThat(previousValue, is(expectedCurrentValue)); + } + ); + } + + @Test + void shouldReplace() + { + testAgainstReference( + (map, keyPartA, keyPartB, expectedCurrentValue, newValue) -> + { + if (expectedCurrentValue == null) + { + assertThat(map.replace(keyPartA, keyPartB, "placeholder", newValue), is(false)); + assertNull(map.put(keyPartA, keyPartB, "placeholder")); + assertThat(map.replace(keyPartA, keyPartB, "placeholder", newValue), is(true)); + } + else + { + assertThat(map.replace(keyPartA, keyPartB, expectedCurrentValue, newValue), is(true)); + } + assertThat(map.replace(keyPartA, keyPartB, newValue, newValue), is(true)); + assertThat(map.replace(keyPartA, keyPartB, "placeholder", newValue + "diff"), is(false)); + }, + (map, keyPartA, keyPartB, expectedCurrentValue) -> + { + final String previousValue = map.remove(keyPartA, keyPartB); + assertThat(previousValue, is(expectedCurrentValue)); + } + ); + } + + @Test + void shouldConditionallyCompute() + { + testAgainstReference( + (map, keyPartA, keyPartB, expectedCurrentValue, newValue) -> + { + if (expectedCurrentValue == null) + { + assertThat(map.computeIfAbsent(keyPartA, keyPartB, (a, b) -> newValue), is(newValue)); + } + else + { + assertThat(map.computeIfPresent(keyPartA, keyPartB, (a, b, current) -> + { + assertThat(current, is(expectedCurrentValue)); + return newValue; + }), is(newValue)); + } + assertThat(map.computeIfAbsent(keyPartA, keyPartB, (a, b) -> "placeholder"), is(newValue)); + }, + (map, keyPartA, keyPartB, expectedCurrentValue) -> + { + assertNull(map.computeIfPresent(keyPartA, keyPartB, (a, b, current) -> + { + assertThat(current, is(expectedCurrentValue)); + return null; + })); + } + ); + } + + @Test + void shouldCompute() + { + testAgainstReference( + (map, keyPartA, keyPartB, expectedCurrentValue, newValue) -> + { + assertThat(map.compute(keyPartA, keyPartB, (a, b, current) -> + { + assertThat(current, is(expectedCurrentValue)); + return newValue; + }), is(newValue)); + }, + (map, keyPartA, keyPartB, expectedCurrentValue) -> + { + assertNull(map.compute(keyPartA, keyPartB, (a, b, current) -> + { + assertThat(current, is(expectedCurrentValue)); + return null; + })); + } + ); + } + + @Test + void shouldMerge() + { + testAgainstReference( + (map, keyPartA, keyPartB, expectedCurrentValue, newValue) -> + { + map.merge(keyPartA, keyPartB, newValue, (oldValue, value) -> + { + assertThat(oldValue, is(expectedCurrentValue)); + assertThat(value, is(newValue)); + return value; + }); + }, + (map, keyPartA, keyPartB, expectedCurrentValue) -> + { + map.merge(keyPartA, keyPartB, "placeholder", (oldValue, value) -> + { + assertThat(oldValue, is(expectedCurrentValue)); + return null; + }); + } + ); + } + + @FunctionalInterface + interface PutFunction + { + void putInMap(BiLong2ObjectMap map, long keyPartA, long keyPartB, + String expectedCurrentValue, String newValue); + } + + @FunctionalInterface + interface RemoveFunction + { + void removeFromMap(BiLong2ObjectMap map, long keyPartA, long keyPartB, String expectedCurrentValue); + } + + static final class EntryCapture + { + public final long keyPartA; + public final long keyPartB; + public final V value; + + EntryCapture(final long keyPartA, final long keyPartB, final V value) + { + this.keyPartA = keyPartA; + this.keyPartB = keyPartB; + this.value = value; + } + + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + + if (!(o instanceof EntryCapture)) + { + return false; + } + + final EntryCapture that = (EntryCapture)o; + + return keyPartA == that.keyPartA && keyPartB == that.keyPartB && value.equals(that.value); + } + + public int hashCode() + { + long result = keyPartA; + result = 31 * result + keyPartB; + result = 31 * result + value.hashCode(); + + return Long.hashCode(result); + } + } + +}