diff --git a/primitiveCollections/build.gradle b/primitiveCollections/build.gradle index 7ef7855..e5ead5d 100644 --- a/primitiveCollections/build.gradle +++ b/primitiveCollections/build.gradle @@ -1,6 +1,6 @@ group='org.lucares' -version = '0.2' +version = '0.3' dependencies { jmh 'org.eclipse.collections:eclipse-collections:10.2.0' diff --git a/primitiveCollections/src/main/java/org/lucares/collections/LongLongHashMap.java b/primitiveCollections/src/main/java/org/lucares/collections/LongLongHashMap.java index 5f3e2ac..d504bc7 100644 --- a/primitiveCollections/src/main/java/org/lucares/collections/LongLongHashMap.java +++ b/primitiveCollections/src/main/java/org/lucares/collections/LongLongHashMap.java @@ -46,6 +46,8 @@ public class LongLongHashMap { */ private Long removedKeyValue = null; + private int maxCapacity = MAX_ARRAY_SIZE; + /** * Create a new {@link LongLongHashMap} with the given initial capacity and load * factor. @@ -78,6 +80,30 @@ public class 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. * @@ -144,9 +170,14 @@ public class LongLongHashMap { * * @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; @@ -448,8 +479,10 @@ public class LongLongHashMap { } private void growAndRehash() { - final int newSize = Math.min(keys.length * 2, MAX_ARRAY_SIZE); - rehash(newSize); + final int newSize = Math.min(keys.length * 2, maxCapacity); + if(newSize != keys.length) { + rehash(newSize); + } } private void rehash(int newSize) { diff --git a/primitiveCollections/src/test/java/org/lucares/collections/LongLongHashMapTest.java b/primitiveCollections/src/test/java/org/lucares/collections/LongLongHashMapTest.java index 98749cd..708ef2d 100644 --- a/primitiveCollections/src/test/java/org/lucares/collections/LongLongHashMapTest.java +++ b/primitiveCollections/src/test/java/org/lucares/collections/LongLongHashMapTest.java @@ -1,5 +1,7 @@ package org.lucares.collections; +import static org.junit.jupiter.api.Assertions.assertThrows; + import java.util.Random; import java.util.stream.LongStream; @@ -54,14 +56,25 @@ public class LongLongHashMapTest { final LongLongHashMap map = new LongLongHashMap(); final int values = 100; - fillMap(map, values); // fill with keys 0...99 + // fill with keys 0...99 + for (int i = 0; i < values; i++) { + map.put(i, i); + } map.remove(values); // key does not exist Assertions.assertEquals(values, map.size(), "size after removing non existing key 100"); + // -1 is a sentinel key and has special handling map.remove(-1); // key does not exist Assertions.assertEquals(values, map.size(), "size after removing non existing key -1"); + + map.put(-1, -1); + Assertions.assertEquals(values+1, map.size(), "size after adding key -1"); + + map.remove(-1); // key exists + Assertions.assertEquals(values, map.size(), "size after removing key -1"); + // 0 is a sentinel key and has special handling map.remove(0); // key exists Assertions.assertEquals(values - 1, map.size(), "size after removing existing key 0"); @@ -70,7 +83,9 @@ public class LongLongHashMapTest { for (int i = 1; i < 100; i++) { map.remove(i); + Assertions.assertEquals(values-i-1, map.size(), "size after removing key "+i); } + Assertions.assertEquals(0, map.size(), "size after removing all keys"); } @Test @@ -352,6 +367,42 @@ public class LongLongHashMapTest { Assertions.assertEquals(5, LongLongHashMap.findPosOfFirstPositiveKey(new long[] { -1, 0, 0, 0, 0, 1, 1 })); Assertions.assertEquals(6, LongLongHashMap.findPosOfFirstPositiveKey(new long[] { -1, 0, 0, 0, 0, 0, 1 })); } + + @Test + public void testMaxCapacity() { + LongLongHashMap map = new LongLongHashMap(6,0.75); + Assertions.assertEquals(6, map.getCapacity()); + + // capacity is reduced to 5 - possible, because map is empty + map.setMaxCapacity(5); + Assertions.assertEquals(5, map.getCapacity()); + + map.put(1, 0); + map.put(2, 0); + map.put(3, 0); + map.put(4, 0); + map.put(5, 0); + Assertions.assertEquals(5, map.getCapacity()); + Assertions.assertEquals(5, map.size()); + + // ensure we cannot add more values than the capacity allows + // 0 and -1 are sentinels, we have to check them separately + assertThrows(IllegalStateException.class, () -> map.put(0, 55)); + assertThrows(IllegalStateException.class, () -> map.put(-1, 55)); + assertThrows(IllegalStateException.class, () -> map.put(6, 55));// key is negative to ensure we actually could add it if the capacity restriction was not there + + Assertions.assertEquals(5, map.size()); // we still have only 5 keys in the map + + // check that we can increase the maxCapacity + map.setMaxCapacity(map.getCapacity()+1); + Assertions.assertEquals(5, map.getCapacity()); // capacity was not updated, because there was not need - you would have to manually call rehash() + Assertions.assertEquals(5, map.size()); + map.put(6, 0); + assertThrows(IllegalStateException.class, () -> map.put(7, 55)); + + // check we cannot make the capacity smaller than the current size + assertThrows(IllegalArgumentException.class, ()->map.setMaxCapacity(map.size()-1)); + } private LongList findKeysWithSameSpread(final LongLongHashMap map) { final LongList result = new LongList(); @@ -369,10 +420,4 @@ public class LongLongHashMapTest { return result; } - - private void fillMap(LongLongHashMap map, int numEntries) { - for (int i = 0; i < numEntries; i++) { - map.put(i, i); - } - } }