add maxCapacity to LongLongHashMap

This allows us to define an upper limit for the memory usage.
This commit is contained in:
2021-04-16 17:52:15 +02:00
parent 9de619d815
commit 062d63ca02
3 changed files with 88 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
group='org.lucares' group='org.lucares'
version = '0.2' version = '0.3'
dependencies { dependencies {
jmh 'org.eclipse.collections:eclipse-collections:10.2.0' jmh 'org.eclipse.collections:eclipse-collections:10.2.0'

View File

@@ -46,6 +46,8 @@ public class LongLongHashMap {
*/ */
private Long removedKeyValue = null; private Long removedKeyValue = null;
private int maxCapacity = MAX_ARRAY_SIZE;
/** /**
* Create a new {@link LongLongHashMap} with the given initial capacity and load * Create a new {@link LongLongHashMap} with the given initial capacity and load
* factor. * factor.
@@ -78,6 +80,30 @@ public class LongLongHashMap {
this(8, 0.75); 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. * The number of entries in this map.
* *
@@ -144,9 +170,14 @@ public class LongLongHashMap {
* *
* @param key the key * @param key the key
* @param value the value * @param value the value
* @throws IllegalStateException if the map is full, see {@link #setMaxCapacity(int)}
*/ */
public void put(final long key, final long value) { public void put(final long key, final long value) {
if (size == maxCapacity) {
throw new IllegalStateException("map is full");
}
if (key == NULL_KEY) { if (key == NULL_KEY) {
size += zeroValue == null ? 1 : 0; size += zeroValue == null ? 1 : 0;
zeroValue = value; zeroValue = value;
@@ -448,8 +479,10 @@ public class LongLongHashMap {
} }
private void growAndRehash() { private void growAndRehash() {
final int newSize = Math.min(keys.length * 2, MAX_ARRAY_SIZE); final int newSize = Math.min(keys.length * 2, maxCapacity);
rehash(newSize); if(newSize != keys.length) {
rehash(newSize);
}
} }
private void rehash(int newSize) { private void rehash(int newSize) {

View File

@@ -1,5 +1,7 @@
package org.lucares.collections; package org.lucares.collections;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Random; import java.util.Random;
import java.util.stream.LongStream; import java.util.stream.LongStream;
@@ -54,14 +56,25 @@ public class LongLongHashMapTest {
final LongLongHashMap map = new LongLongHashMap(); final LongLongHashMap map = new LongLongHashMap();
final int values = 100; 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 map.remove(values); // key does not exist
Assertions.assertEquals(values, map.size(), "size after removing non existing key 100"); 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 map.remove(-1); // key does not exist
Assertions.assertEquals(values, map.size(), "size after removing non existing key -1"); 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 map.remove(0); // key exists
Assertions.assertEquals(values - 1, map.size(), "size after removing existing key 0"); 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++) { for (int i = 1; i < 100; i++) {
map.remove(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 @Test
@@ -353,6 +368,42 @@ public class LongLongHashMapTest {
Assertions.assertEquals(6, LongLongHashMap.findPosOfFirstPositiveKey(new long[] { -1, 0, 0, 0, 0, 0, 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) { private LongList findKeysWithSameSpread(final LongLongHashMap map) {
final LongList result = new LongList(); final LongList result = new LongList();
final int spread = map.spread(1); final int spread = map.spread(1);
@@ -369,10 +420,4 @@ public class LongLongHashMapTest {
return result; return result;
} }
private void fillMap(LongLongHashMap map, int numEntries) {
for (int i = 0; i < numEntries; i++) {
map.put(i, i);
}
}
} }