diff --git a/build.gradle b/build.gradle index 51bfca8..36a2699 100644 --- a/build.gradle +++ b/build.gradle @@ -20,8 +20,16 @@ plugins { apply plugin: 'java' apply plugin: 'eclipse' + +ext { + javaVersion=11 + version_junit = '5.7.1' + version_junit_platform = '1.7.1' +} + + // java compatibility version -sourceCompatibility = 11 +sourceCompatibility = javaVersion /* * The shared configuration for all sub-projects: @@ -76,9 +84,10 @@ subprojects { // dependencies that all sub-projects have dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' - testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.7.0' - testRuntime 'org.junit.platform:junit-platform-launcher:1.7.0' // needed by eclipse + testImplementation "org.junit.jupiter:junit-jupiter-api:${version_junit}" + testImplementation "org.junit.jupiter:junit-jupiter-params:${version_junit}" // for @ParameterizedTest + testRuntime "org.junit.jupiter:junit-jupiter-engine:${version_junit}" + testRuntime "org.junit.platform:junit-platform-launcher:${version_junit_platform}" // needed by eclipse } test { diff --git a/primitiveCollections/src/main/java/org/lucares/collections/BiLongFunction.java b/primitiveCollections/src/main/java/org/lucares/collections/BiLongFunction.java new file mode 100644 index 0000000..fbb5bfc --- /dev/null +++ b/primitiveCollections/src/main/java/org/lucares/collections/BiLongFunction.java @@ -0,0 +1,6 @@ +package org.lucares.collections; + +@FunctionalInterface +public interface BiLongFunction { + long apply(long key, long value); +} diff --git a/primitiveCollections/src/main/java/org/lucares/collections/LongFunction.java b/primitiveCollections/src/main/java/org/lucares/collections/LongFunction.java deleted file mode 100644 index 76de520..0000000 --- a/primitiveCollections/src/main/java/org/lucares/collections/LongFunction.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.lucares.collections; - -@FunctionalInterface -public interface LongFunction { - long apply(long value); -} diff --git a/primitiveCollections/src/main/java/org/lucares/collections/LongLongHashMap.java b/primitiveCollections/src/main/java/org/lucares/collections/LongLongHashMap.java index 7b4f7a0..5f3e2ac 100644 --- a/primitiveCollections/src/main/java/org/lucares/collections/LongLongHashMap.java +++ b/primitiveCollections/src/main/java/org/lucares/collections/LongLongHashMap.java @@ -1,5 +1,6 @@ package org.lucares.collections; + import java.util.Arrays; /** @@ -12,7 +13,13 @@ 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. - private static final long NULL_KEY = 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; @@ -27,14 +34,24 @@ public class LongLongHashMap { 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; /** * Create a new {@link LongLongHashMap} with the given initial capacity and load * factor. * * @param initialCapacity the initial capacity - * @param loadFactor the load factor + * @param loadFactor the load factor between 0 and 1 */ public LongLongHashMap(final int initialCapacity, final double loadFactor) { @@ -44,8 +61,9 @@ public class LongLongHashMap { 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)) - throw new IllegalArgumentException("Illegal load factor: " + loadFactor); + 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]; @@ -68,6 +86,49 @@ public class LongLongHashMap { 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. @@ -91,6 +152,12 @@ public class LongLongHashMap { zeroValue = value; return; } + + if (key == REMOVED_KEY) { + size += removedKeyValue == null ? 1 : 0; + removedKeyValue = value; + return; + } if ((keys.length * fillFactor) < size) { growAndRehash(); @@ -122,7 +189,13 @@ public class LongLongHashMap { currentPosition = (currentPosition + 1) % keys.length; } while (currentPosition != searchStart); - throw new IllegalStateException("map is full"); + // 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); } /** @@ -140,16 +213,24 @@ public class LongLongHashMap { } 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; + return defaultValue;// should never be reached unless the map is full at 100%, which should be impossible with loadFactor < 1.0 } /** @@ -163,16 +244,21 @@ public class LongLongHashMap { 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; + } while (currentPosition != searchStart); + return false;// should never be reached unless the map is full at 100%, which should be impossible with loadFactor < 1.0 } /** @@ -187,17 +273,25 @@ public class LongLongHashMap { 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] = EMPTY_SLOT; + 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); + } while (currentPosition != searchStart); // run around should never happen unless the map is full at 100% } /** @@ -213,13 +307,21 @@ public class LongLongHashMap { * key * @param function called to update an existing value */ - public void compute(final long key, final long initialValueIfAbsent, final LongFunction function) { + public void compute(final long key, final long initialValueIfAbsent, final BiLongFunction function) { if (key == NULL_KEY) { if (zeroValue != null) { - zeroValue = function.apply(zeroValue); + zeroValue = function.apply(NULL_KEY,zeroValue); return; } - zeroValue = function.apply(initialValueIfAbsent); + 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; } @@ -227,15 +329,19 @@ public class LongLongHashMap { int currentPosition = searchStart; do { if (keys[currentPosition] == key) { - final long updatedValue = function.apply(values[currentPosition]); + 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(initialValueIfAbsent); + final long newZeroValue = function.apply(key, initialValueIfAbsent); put(key, newZeroValue); } @@ -249,11 +355,14 @@ public class LongLongHashMap { public void forEach(final LongLongConsumer consumer) { if (zeroValue != null) { - consumer.accept(0, zeroValue); + 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) { + if (keys[i] != EMPTY_SLOT && keys[i] != REMOVED_KEY) { consumer.accept(keys[i], values[i]); } } @@ -268,26 +377,36 @@ public class LongLongHashMap { * @param consumer the consumer */ public void forEachOrdered(final LongLongConsumer consumer) { - - if (zeroValue != null) { - consumer.accept(0, zeroValue); - } - 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 != EMPTY_SLOT) { + if (key < REMOVED_KEY) { consumer.accept(key, get(key, 0)); // the default value of 'get' will not be used, because the key exists - } else if (key == EMPTY_SLOT) { - final int posFirstKey = findPosOfFirstPositiveKey(sortedKeys); - if (posFirstKey < 0) { - return; - } - i = posFirstKey - 1; + } 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) { @@ -318,19 +437,30 @@ public class LongLongHashMap { 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, MAX_ARRAY_SIZE);
+ rehash(newSize);
+ }
+
+ private void rehash(int newSize) {
final long[] oldKeys = keys;
final long[] oldValues = values;
-
- final int newSize = Math.min(keys.length * 2, MAX_ARRAY_SIZE);
-
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) {
+ if (key != EMPTY_SLOT && key != REMOVED_KEY) {
final long value = oldValues[i];
putInternal(key, value);
}
diff --git a/primitiveCollections/src/main/java/org/lucares/collections/LongObjHashMap.java b/primitiveCollections/src/main/java/org/lucares/collections/LongObjHashMap.java
index e42bc87..df90531 100644
--- a/primitiveCollections/src/main/java/org/lucares/collections/LongObjHashMap.java
+++ b/primitiveCollections/src/main/java/org/lucares/collections/LongObjHashMap.java
@@ -14,7 +14,12 @@ public class LongObjHashMap
+ * This is a maintenance operation that should be executed periodically after removing elements.
+ */
+ public void rehash() {
+ rehash(keys.length);
+ }
- @SuppressWarnings("unchecked")
private void growAndRehash() {
+ final int newSize = Math.min(keys.length * 2, MAX_ARRAY_SIZE);
+ rehash(newSize);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void rehash(int newSize) {
final long[] oldKeys = keys;
final V[] oldValues = values;
- final int newSize = Math.min(keys.length * 2, MAX_ARRAY_SIZE);
-
keys = new long[newSize];
values = (V[]) new Object[newSize];
for (int i = 0; i < oldKeys.length; i++) {
final long key = oldKeys[i];
- if (key != EMPTY_SLOT) {
+ if (key != EMPTY_SLOT && key != REMOVED_KEY) {
final V value = oldValues[i];
putInternal(key, value);
}
diff --git a/primitiveCollections/src/test/java/org/lucares/collections/LongLongHashMapTest.java b/primitiveCollections/src/test/java/org/lucares/collections/LongLongHashMapTest.java
index 9bf7309..98749cd 100644
--- a/primitiveCollections/src/test/java/org/lucares/collections/LongLongHashMapTest.java
+++ b/primitiveCollections/src/test/java/org/lucares/collections/LongLongHashMapTest.java
@@ -16,6 +16,11 @@ public class LongLongHashMapTest {
public void testNullValue() {
putGetRemove(0);
}
+
+ @Test
+ public void testRemovedValue() {
+ putGetRemove(-1);
+ }
private void putGetRemove(final long key) {
final LongLongHashMap map = new LongLongHashMap();
@@ -45,26 +50,108 @@ public class LongLongHashMapTest {
}
@Test
- public void testComputeZeroKey() {
+ public void testRemoveNonExistingKey() {
final LongLongHashMap map = new LongLongHashMap();
+ final int values = 100;
- final long key = 1;
- map.compute(key, 6, l -> l + 1);
- Assertions.assertEquals(7, map.get(key, Long.MIN_VALUE), "initialValueIfAbsent is used when there is no mapping for the key");
+ fillMap(map, values); // fill with keys 0...99
- map.compute(key, 6, l -> l + 1);
- Assertions.assertEquals(8, map.get(key, Long.MIN_VALUE), "update function is called when 'zeroKey' is set");
+ map.remove(values); // key does not exist
+ Assertions.assertEquals(values, map.size(), "size after removing non existing key 100");
+
+ map.remove(-1); // key does not exist
+ Assertions.assertEquals(values, map.size(), "size after removing non existing key -1");
+
+ map.remove(0); // key exists
+ Assertions.assertEquals(values - 1, map.size(), "size after removing existing key 0");
+
+ map.remove(0); // key does not exist
+ Assertions.assertEquals(values - 1, map.size(), "size after removing non existing key 0 (2nd removal of 0)");
+
+ for (int i = 1; i < 100; i++) {
+ map.remove(i);
+ }
}
@Test
- public void testCompute() {
+ public void testRemoveAllValuesOneByOne() {
+ int values = 20;
final LongLongHashMap map = new LongLongHashMap();
- final long key = 1;
- map.compute(key, 6, l -> l + 1);
- Assertions.assertEquals(7, map.get(key, Long.MIN_VALUE), "initialValueIfAbsent is used when there is no mapping for the key");
+ Random r = new Random(123);
- map.compute(key, 6, l -> l + 1);
- Assertions.assertEquals(8, map.get(key, Long.MIN_VALUE), "update function is called when key is set");
+ LongList keys = new LongList();
+
+ for (int round = 0; round < 5; round++) {
+ values *= 2;
+ keys.clear();
+ keys.addAll(r.longs(values).toArray());
+ keys.shuffle(r);
+ keys.stream().forEach(l -> map.put(l, 2));
+
+ for (int i = values - 1; i >= 0; i--) {
+ long key = keys.get(i);
+
+ map.compute(key, 2, (k,v) -> v * 2);
+ Assertions.assertEquals(4, map.get(key, -1), "value for key " + key + "=4 - map=" + map);
+
+ Assertions.assertTrue(map.containsKey(key), "map contains key " + key);
+ map.remove(key);
+ Assertions.assertEquals(i, map.size(), "size after removing key " + key);
+ }
+ }
+ }
+
+
+ @Test
+ public void testCompute() {
+ final LongLongHashMap map = new LongLongHashMap();
+
+ // initialize values
+ map.compute(LongLongHashMap.REMOVED_KEY, 11L, (k,v) -> {
+ Assertions.assertEquals(LongLongHashMap.REMOVED_KEY, k);
+ Assertions.assertEquals(11, v);
+ return 12L;
+ });
+ Assertions.assertEquals(12, map.get(LongLongHashMap.REMOVED_KEY, -111),
+ "initialValueIfAbsent is used when there is no mapping for the key");
+
+ map.compute(LongLongHashMap.NULL_KEY, 21L, (k,v) -> {
+ Assertions.assertEquals(LongLongHashMap.NULL_KEY, k);
+ Assertions.assertEquals(21, v);
+ return 22L;
+ });
+ Assertions.assertEquals(22, map.get(LongLongHashMap.NULL_KEY, -111),
+ "initialValueIfAbsent is used when there is no mapping for the key");
+
+ map.compute(1, 31L, (k,v) -> {
+ Assertions.assertEquals(1, k);
+ Assertions.assertEquals(31, v);
+ return 32L;
+ });
+ Assertions.assertEquals(32, map.get(1, -111),
+ "initialValueIfAbsent is used when there is no mapping for the key");
+
+ // update the value
+ map.compute(LongLongHashMap.REMOVED_KEY, -123L, (k,v) -> {
+ Assertions.assertEquals(LongLongHashMap.REMOVED_KEY, k);
+ Assertions.assertEquals(12, v);
+ return 13L;
+ });
+ Assertions.assertEquals(13, map.get(LongLongHashMap.REMOVED_KEY, -111), "update function is called when key is set");
+
+ map.compute(LongLongHashMap.NULL_KEY, -123L, (k,v) -> {
+ Assertions.assertEquals(LongLongHashMap.NULL_KEY, k);
+ Assertions.assertEquals(22, v);
+ return 23L;
+ });
+ Assertions.assertEquals(23, map.get(LongLongHashMap.NULL_KEY, -111), "update function is called when key is set");
+
+ map.compute(1, -123L, (k,v) -> {
+ Assertions.assertEquals(1, k);
+ Assertions.assertEquals(32, v);
+ return 33L;
+ });
+ Assertions.assertEquals(33, map.get(1, -111), "update function is called when key is set");
}
@Test
@@ -96,7 +183,41 @@ public class LongLongHashMapTest {
keysWithSameSpread.stream().forEach(l -> map.put(l, l));
Assertions.assertEquals(keysWithSameSpread.size(), map.size());
keysWithSameSpread.stream().forEach(l -> Assertions.assertEquals(l, map.get(l, Long.MIN_VALUE)));
+
+ // remove the keys
+ keysWithSameSpread.shuffle();
+ keysWithSameSpread.stream().forEach(k -> {
+ int sizeBefore = map.size();
+ Assertions.assertTrue(map.containsKey(k));
+ map.remove(k);
+ Assertions.assertFalse(map.containsKey(k));
+ Assertions.assertEquals(Long.MIN_VALUE, map.get(k, Long.MIN_VALUE));
+ Assertions.assertEquals(sizeBefore - 1, map.size());
+ });
}
+
+ @Test
+ public void testMultipleValuesOnSamePosition2() {
+ final LongLongHashMap map = new LongLongHashMap();
+ // find to values that yield the same 'spread' (position in the table)
+ final LongList keys = findKeysWithSameSpread(map);
+ Assertions.assertTrue(keys.size() > 5);
+
+ map.put(keys.get(0), 1);
+ map.put(keys.get(1), 1);
+ map.put(keys.get(2), 1);
+
+ // creates a section of the array that looks like this: k0,-1,k2, where -1 marks a previously occupied slot
+ map.remove(keys.get(1));
+
+ // should overwrite the existing value which is after a slot that is marked as previously occupied
+ map.put(keys.get(2), 2);
+
+ final LongList values=new LongList();
+ map.forEach((k,v) -> values.add(v));
+ Assertions.assertEquals(LongList.of(1,2), values);
+ }
+
@Test
public void testForEach() {
@@ -104,7 +225,7 @@ public class LongLongHashMapTest {
final Random rand = new Random(6789);
final LongList entries = LongList.of(LongStream.generate(rand::nextLong).limit(15).toArray());
- entries.stream().forEachOrdered(l -> {
+ entries.stream().forEach(l -> {
map.put(l, 2 * l);
});
@@ -115,10 +236,29 @@ public class LongLongHashMapTest {
}
@Test
- public void testForEachOrdered() {
+ public void testForEachWithSpecialValues() {
final LongLongHashMap map = new LongLongHashMap();
final Random rand = new Random(6789);
final LongList entries = LongList.of(LongStream.generate(rand::nextLong).limit(15).toArray());
+ entries.add(0); // special key that is internally used to mark unset slots
+ entries.add(-1);// special key that is internally used to mark slots with removed values
+ entries.add(123); // value that will be removed later
+
+ entries.stream().forEach(l -> {
+ map.put(l, 2 * l);
+ });
+ map.remove(123);
+
+ map.forEach((k, v) -> {
+ Assertions.assertEquals(k * 2, v, "value is key*2");
+ Assertions.assertTrue(entries.indexOf(k) >= 0, "value " + k + " in entries: " + entries);
+ });
+ }
+
+ @Test
+ public void testForEachOrdered() {
+ final LongLongHashMap map = new LongLongHashMap();
+ final LongList entries = LongList.of(-10, -9, -1, 0, 1, 2, 10);
entries.stream().forEachOrdered(l -> {
map.put(l, 2 * l);
@@ -229,4 +369,10 @@ public class LongLongHashMapTest {
return result;
}
+
+ private void fillMap(LongLongHashMap map, int numEntries) {
+ for (int i = 0; i < numEntries; i++) {
+ map.put(i, i);
+ }
+ }
}
diff --git a/primitiveCollections/src/test/java/org/lucares/collections/LongObjHashMapTest.java b/primitiveCollections/src/test/java/org/lucares/collections/LongObjHashMapTest.java
index f38e7a8..6295ee7 100644
--- a/primitiveCollections/src/test/java/org/lucares/collections/LongObjHashMapTest.java
+++ b/primitiveCollections/src/test/java/org/lucares/collections/LongObjHashMapTest.java
@@ -1,7 +1,6 @@
package org.lucares.collections;
import java.util.Random;
-import java.util.function.Supplier;
import java.util.stream.LongStream;
import org.junit.jupiter.api.Assertions;
@@ -9,19 +8,6 @@ import org.junit.jupiter.api.Test;
public class LongObjHashMapTest {
- private static final class LongSupplier implements Supplier