From 2b59acc202c43a1562079f15299e5850b051d389 Mon Sep 17 00:00:00 2001 From: psavin Date: Fri, 5 May 2023 20:40:56 +0300 Subject: [PATCH 1/5] Initial impl + basic unit tests --- .../opensource/longmap/LongMapImpl.java | 281 +++++++++++++++++- .../longmap/LongMapImplBasicTest.java | 236 +++++++++++++++ 2 files changed, 509 insertions(+), 8 deletions(-) create mode 100644 src/test/java/de/comparus/opensource/longmap/LongMapImplBasicTest.java diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index 2f0b54b..f358979 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -1,43 +1,308 @@ package de.comparus.opensource.longmap; -public class LongMapImpl implements LongMap { +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Objects; + +public class LongMapImpl implements LongMap, Iterable.LongBucket> { + + private static final int ARRAY_MAX_SIZE = Integer.MAX_VALUE - 8; + private static final int DEFAULT_CAPACITY = 8; + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + private static final int RESIZE_FACTOR = 2; + final float loadFactor; + private int bucketCount; + private LongBucket[] buckets; + + public LongMapImpl() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + public LongMapImpl(int capacity, float loadFactor) { + this.loadFactor = loadFactor; + this.buckets = new LongBucket[capacity]; + } + public V put(long key, V value) { - return null; + if (bucketCount == ARRAY_MAX_SIZE) { + throw new IllegalStateException(String.format( + "Current instance of Map is overloaded. It can only store %s mappings at max.", + ARRAY_MAX_SIZE + )); + } + + if (isThresholdExceeded(key)) { + resize(); + } + + int bucketIndex = getIndex(key); + LongBucket currentBucket = buckets[bucketIndex]; + if (currentBucket == null) { + buckets[bucketIndex] = new LongBucket<>(key, value); + bucketCount++; + return null; + } + + return currentBucket.put(key, value); + } + + private boolean isThresholdExceeded(long key) { + return !containsKey(key) + && (buckets.length == 0 || (int) (buckets.length * loadFactor) <= bucketCount); + } + + private void resize() { + if (buckets.length == ARRAY_MAX_SIZE) { + return; + } + + LongBucket[] newBuckets = new LongBucket[getNewSize()]; + for (LongBucket currentBucket : buckets) { + while (currentBucket != null) { + int newIndex = getIndex(currentBucket.getKey(), newBuckets); + LongBucket collision = currentBucket.getCollision(); + currentBucket.setCollision(null); + + //possible collision + LongBucket rehashedBucket = newBuckets[newIndex]; + if (rehashedBucket == null) { + newBuckets[newIndex] = currentBucket; + } else { + rehashedBucket.collide(currentBucket); + } + currentBucket = collision; + } + } + this.buckets = newBuckets; + } + + int getNewSize() { + if (buckets.length == 0) { + return DEFAULT_CAPACITY; + } + return ARRAY_MAX_SIZE / RESIZE_FACTOR > buckets.length + ? buckets.length * RESIZE_FACTOR + : ARRAY_MAX_SIZE; } public V get(long key) { - return null; + int index = getIndex(key); + return buckets[index] == null + ? null + : buckets[index].get(key); } public V remove(long key) { + int index = getIndex(key); + + LongBucket currentBucket = buckets[index]; + if (currentBucket != null) { + if (currentBucket.getKey() == key) { + V result = currentBucket.getValue(); + buckets[index] = currentBucket.getCollision(); + bucketCount--; + return result; + } else { + return currentBucket.remove(key); + } + } return null; } public boolean isEmpty() { - return false; + return bucketCount == 0; } public boolean containsKey(long key) { - return false; + return get(key) != null; } public boolean containsValue(V value) { + for (LongBucket bucket : this) { + if (bucket.getValue() == value || (bucket.getValue() != null && bucket.getValue().equals(value))) { + return true; + } + } return false; } public long[] keys() { - return null; + if (bucketCount == 0) { + //just to stay consistent with values() + return null; + } + long[] keys = new long[bucketCount]; + int keyIndex = bucketCount; + for (LongBucket bucket : this) { + keys[--keyIndex] = bucket.getKey(); + } + return keys; } public V[] values() { - return null; + if (bucketCount == 0) { + //impossible to instantiate a generic array since element type is not known at runtime + return null; + } + V[] values = (V[]) Array.newInstance(this.iterator().next().getValue().getClass(), bucketCount); + int valueIndex = bucketCount; + for (LongBucket bucket : this) { + values[--valueIndex] = bucket.getValue(); + } + return values; } public long size() { - return 0; + return bucketCount; } public void clear() { + Arrays.fill(buckets, null); + bucketCount = 0; + } + + private int getIndex(long key) { + return getIndex(key, buckets); + } + + private int getIndex(long key, LongBucket[] storage) { + return (Long.hashCode(key) & 0x7FFFFFFF) % storage.length; + } + + int getCapacity() { + return buckets.length; + } + + @Override + public Iterator> iterator() { + return new CollisionAwareLongBucketIterator(); + } + + class CollisionAwareLongBucketIterator implements Iterator> { + private LongBucket next; + private int currentIndex = 0; + + public CollisionAwareLongBucketIterator() { + next = getNext(); + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public LongBucket next() { + if (next == null) { + throw new IllegalStateException("There is no next element to iterate upon."); + } + + LongBucket current = next; + next = getNext(); + return current; + } + + private LongBucket getNext() { + if (next != null && next.getCollision() != null) { + return next.getCollision(); + } + + while (currentIndex < buckets.length) { + if (buckets[currentIndex] != null) { + return buckets[currentIndex++]; + } + currentIndex++; + } + return null; + } + } + + class LongBucket { + private final long key; + private T value; + private LongBucket collision; + + private LongBucket(long key, T value) { + this.key = key; + this.value = value; + } + + public final long getKey() { + return key; + } + + public final T getValue() { + return value; + } + + public final String toString() { + return key + "=" + value; + } + + public final int hashCode() { + return Objects.hash(key, value); + } + + public final void setValue(T newValue) { + this.value = newValue; + } + + public LongBucket getCollision() { + return collision; + } + + public void setCollision(LongBucket collision) { + this.collision = collision; + } + + public T remove(long key) { + if (collision != null) { + if (collision.getKey() == key) { + T removed = collision.getValue(); + collision = collision.getCollision(); + bucketCount--; + return removed; + } else { + return collision.remove(key); + } + } + return null; + } + + public T get(long key) { + if (this.key == key) { + return this.value; + } + if (collision != null) { + return collision.get(key); + } + return null; + } + + public T put(long key, T value) { + if (this.getKey() == key) { + T result = this.getValue(); + this.setValue(value); + return result; + } + + if (collision != null) { + return collision.put(key, value); + } + + collision = new LongBucket<>(key, value); + bucketCount++; + return null; + } + + public T collide(LongBucket anotherBucket) { + if (collision != null) { + return collision.collide(anotherBucket); + } + collision = anotherBucket; + return null; + } } } diff --git a/src/test/java/de/comparus/opensource/longmap/LongMapImplBasicTest.java b/src/test/java/de/comparus/opensource/longmap/LongMapImplBasicTest.java new file mode 100644 index 0000000..e5fa0fa --- /dev/null +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplBasicTest.java @@ -0,0 +1,236 @@ +package de.comparus.opensource.longmap; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Random; +import java.util.stream.LongStream; + +import static org.junit.Assert.*; + +public class LongMapImplBasicTest { + + private static final String VALUE_PREFIX = "value_"; + private LongMapImpl instance; + + @Before + public void init() { + instance = new LongMapImpl<>(); + } + + @Test + public void when_put_new_then_store_correctly() { + + putRandom(100); + + assertEquals(100, instance.size()); + assertEquals(256, instance.getCapacity()); + } + + @Test + public void when_put_existing_then_update_correctly() { + + int mappingsNumber = 100; + long[] keys = putRandom(mappingsNumber); + Arrays.stream(keys) + .forEach(key -> { + String newValue = VALUE_PREFIX + key + "updated"; + String oldValue = instance.put(key, newValue); + String updatedValue = instance.get(key); + + assertEquals(VALUE_PREFIX + key, oldValue); + assertEquals(newValue, updatedValue); + }); + assertEquals(mappingsNumber, instance.size()); + assertEquals(256, instance.getCapacity()); + } + + @Test + public void when_get_then_return_correctly() { + + long[] keys = putRandom(100); + Arrays.stream(keys) + .forEach(key -> { + String value = instance.get(key); + assertNotNull(value); + assertEquals(VALUE_PREFIX + key, value); + }); + } + + @Test + public void when_get_and_map_is_empty_then_return_null() { + assertNull(instance.get(100L)); + } + + @Test + public void when_get_with_wrong_key_then_return_null() { + put(6); + assertNull(instance.get(7)); + } + + @Test + public void when_remove_then_delete_correctly() { + + int mappingsNumber = 100; + int toBeRemoved = 25; + long[] keys = putRandom(mappingsNumber); + Arrays.stream(keys) + .skip(50) + .limit(toBeRemoved) + .forEach(key -> { + String removed = instance.remove(key); + assertNotNull(removed); + assertEquals(VALUE_PREFIX + key, removed); + assertNull(instance.get(key)); + }); + + assertEquals(mappingsNumber - toBeRemoved, instance.size()); + } + + @Test + public void when_remove_with_wrong_key_then_no_delete() { + put(6); + String removed = instance.remove(10); + + assertNull(removed); + assertEquals(6, instance.size()); + } + + @Test + public void when_empty_then_return_true_otherwise_false() { + + assertTrue(instance.isEmpty()); + + put(2); + assertFalse(instance.isEmpty()); + + instance.remove(0); + instance.remove(1); + assertTrue(instance.isEmpty()); + + put(2); + instance.clear(); + assertTrue(instance.isEmpty()); + + put(2); + instance.remove(0); + assertFalse(instance.isEmpty()); + } + + @Test + public void when_contains_key_then_return_true_otherwise_false() { + long[] keys = putRandom(10); + assertTrue(instance.containsKey(keys[5])); + + instance.remove(keys[5]); + assertFalse(instance.containsKey(keys[5])); + + instance.clear(); + assertFalse(instance.containsKey(keys[5])); + } + + @Test + public void when_contains_value_then_return_true_otherwise_false() { + long[] keys = putRandom(10); + String valueOne = VALUE_PREFIX + keys[3]; + String valueTwo = VALUE_PREFIX + keys[7]; + assertTrue(instance.containsValue(valueOne)); + assertTrue(instance.containsValue(valueTwo)); + assertFalse(instance.containsValue(VALUE_PREFIX + "wrong")); + + instance.remove(keys[3]); + assertFalse(instance.containsValue(valueOne)); + + instance.clear(); + assertFalse(instance.containsValue(valueTwo)); + } + + @Test + public void when_get_keys_then_return_correct_keys_array() { + + long[] expectedKeys = Arrays.stream(putRandom(40)).sorted().toArray(); + long[] actualKeys = Arrays.stream(instance.keys()).sorted().toArray(); + assertEquals(instance.size(), actualKeys.length); + assertArrayEquals(expectedKeys, actualKeys); + + instance.remove(expectedKeys[10]); + instance.remove(expectedKeys[25]); + + long[] actualUpdatedKeys = Arrays.stream(instance.keys()).sorted().toArray(); + long[] expectedUpdatedKeys = Arrays.stream(expectedKeys) + .filter(key -> key != expectedKeys[10] && key != expectedKeys[25]) + .sorted() + .toArray(); + assertEquals(instance.size(), expectedUpdatedKeys.length); + assertArrayEquals(expectedUpdatedKeys, actualUpdatedKeys); + + instance.clear(); + long[] emptyKeys = instance.keys(); + assertNull(emptyKeys); + } + + @Test + public void when_get_values_then_return_correct_value_array() { + int mappingsNumber = 20; + long[] keys = putRandom(mappingsNumber); + + String[] actualValues = Arrays.stream(instance.values()).sorted().toArray(String[]::new); + String[] expectedValues = Arrays.stream(keys) + .mapToObj(key -> VALUE_PREFIX + key) + .sorted() + .toArray(String[]::new); + assertEquals(instance.size(), actualValues.length); + assertArrayEquals(expectedValues, actualValues); + + instance.remove(keys[11]); + instance.remove(keys[19]); + + String[] actualUpdatedValues = Arrays.stream(instance.values()).sorted().toArray(String[]::new); + String[] expectedUpdatedValues = Arrays.stream(keys) + .filter(key -> key != keys[11] && key != keys[19]) + .mapToObj(key -> VALUE_PREFIX + key) + .sorted() + .toArray(String[]::new); + assertEquals(actualUpdatedValues.length, expectedUpdatedValues.length); + assertEquals(instance.size(), actualUpdatedValues.length); + assertArrayEquals(expectedUpdatedValues, actualUpdatedValues); + + instance.clear(); + String[] emptyValues = instance.values(); + assertNull(emptyValues); + } + + @Test + public void when_get_size_return_correct_value() { + assertEquals(0, instance.size()); + + long[] keys = putRandom(100); + assertEquals(100, instance.size()); + + instance.remove(keys[2]); + instance.remove(keys[10]); + instance.remove(keys[59]); + assertEquals(97, instance.size()); + + instance.clear(); + assertEquals(0, instance.size()); + } + + + private void put(long mappingsNumber) { + LongStream + .range(0L, mappingsNumber) + .forEach(key -> instance.put(key, VALUE_PREFIX + key)); + } + + private long[] putRandom(long mappingsNumber) { + Random random = new Random(); + return LongStream + .range(0L, mappingsNumber) + .map(l -> random.nextLong()) + .peek(key -> instance.put(key, VALUE_PREFIX + key)) + .toArray(); + } + +} \ No newline at end of file From 428be469de95045f2a734c0cc4d5e942b06a756c Mon Sep 17 00:00:00 2001 From: psavin Date: Fri, 5 May 2023 21:38:21 +0300 Subject: [PATCH 2/5] collisions test --- .../longmap/LongMapImplCollisionsTest.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/test/java/de/comparus/opensource/longmap/LongMapImplCollisionsTest.java diff --git a/src/test/java/de/comparus/opensource/longmap/LongMapImplCollisionsTest.java b/src/test/java/de/comparus/opensource/longmap/LongMapImplCollisionsTest.java new file mode 100644 index 0000000..89ed8d1 --- /dev/null +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplCollisionsTest.java @@ -0,0 +1,93 @@ +package de.comparus.opensource.longmap; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Random; +import java.util.stream.LongStream; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertNull; + +public class LongMapImplCollisionsTest { + + private static final String VALUE_PREFIX = "value_"; + LongMapImpl instance; + + @Before + public void init() { + //prevent resizing, enforce collisions + instance = new LongMapImpl<>(16, 100.0F); + } + + @Test + public void when_put_with_collisions_then_store_correctly() { + putRandom(200); + + assertEquals(16, instance.getCapacity()); + assertEquals(200, instance.size()); + } + + @Test + public void when_get_with_collisions_then_return_correctly() { + long[] keys = putRandom(200); + Arrays.stream(keys).forEach(key -> { + String value = instance.get(key); + assertEquals(VALUE_PREFIX + key, value); + }); + assertEquals(16, instance.getCapacity()); + assertEquals(200, instance.size()); + } + + @Test + public void when_get_keys_with_collisions_then_return_correctly() { + long[] expectedKeys = Arrays.stream(putRandom(200)).sorted().toArray(); + long[] actualKeys = Arrays.stream(instance.keys()).sorted().toArray(); + + assertArrayEquals(expectedKeys, actualKeys); + assertEquals(16, instance.getCapacity()); + assertEquals(200, instance.size()); + } + + @Test + public void when_get_values_with_collisions_then_return_correctly() { + long[] keys = putRandom(200); + String[] actualValues = Arrays.stream(instance.values()).sorted().toArray(String[]::new); + String[] expectedValues = Arrays.stream(keys).mapToObj(key -> VALUE_PREFIX + key).sorted().toArray(String[]::new); + + assertArrayEquals(expectedValues, actualValues); + assertEquals(16, instance.getCapacity()); + assertEquals(200, instance.size()); + } + + @Test + public void when_remove_values_with_collisions_then_delete_correctly() { + int mappingsNumber = 200; + int toBeRemoved = 45; + long[] keys = putRandom(mappingsNumber); + Arrays.stream(keys) + .skip(150) + .limit(toBeRemoved) + .forEach(key -> { + String removed = instance.remove(key); + assertNotNull(removed); + assertEquals(VALUE_PREFIX + key, removed); + assertNull(instance.get(key)); + }); + + assertEquals(mappingsNumber - toBeRemoved, instance.size()); + assertEquals(16, instance.getCapacity()); + } + + private long[] putRandom(long mappingsNumber) { + Random random = new Random(); + return LongStream + .range(0L, mappingsNumber) + .map(l -> random.nextLong()) + .peek(key -> instance.put(key, VALUE_PREFIX + key)) + .toArray(); + } + +} \ No newline at end of file From 19fe19db8e7c04fba04c9313c59e2f26a7288d02 Mon Sep 17 00:00:00 2001 From: psavin Date: Fri, 5 May 2023 21:38:56 +0300 Subject: [PATCH 3/5] cosmetic changes --- .../comparus/opensource/longmap/LongMapImplCollisionsTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/de/comparus/opensource/longmap/LongMapImplCollisionsTest.java b/src/test/java/de/comparus/opensource/longmap/LongMapImplCollisionsTest.java index 89ed8d1..0a5febe 100644 --- a/src/test/java/de/comparus/opensource/longmap/LongMapImplCollisionsTest.java +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplCollisionsTest.java @@ -1,6 +1,5 @@ package de.comparus.opensource.longmap; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -9,7 +8,6 @@ import java.util.stream.LongStream; import static org.junit.Assert.*; -import static org.junit.Assert.assertNull; public class LongMapImplCollisionsTest { From 41af0e5de81887a04e1160a84e168220dc8d4941 Mon Sep 17 00:00:00 2001 From: psavin Date: Fri, 5 May 2023 22:40:03 +0300 Subject: [PATCH 4/5] Minor adjustments + java docs --- .../opensource/longmap/LongMapImpl.java | 172 +++++++++++------- .../longmap/LongMapImplBasicTest.java | 9 + 2 files changed, 112 insertions(+), 69 deletions(-) diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index f358979..b3689de 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -5,8 +5,16 @@ import java.util.Iterator; import java.util.Objects; +/** + * Since Arrays have int based index - it is not possible to accommodate all possible range of long keys into such. + * Thus, internal storage is bounded to Integer.MAX_VALUE - 8 max size. + * Also since keys() and values() return an array this implementation should only be capable of storing a subset of long keys range. + * In case max size is exceeded - an exception is thrown upon attempt to put new key value pairs. + * This implementation is not thread safe. + */ public class LongMapImpl implements LongMap, Iterable.LongBucket> { + /*https://stackoverflow.com/questions/3038392/do-java-arrays-have-a-maximum-size*/ private static final int ARRAY_MAX_SIZE = Integer.MAX_VALUE - 8; private static final int DEFAULT_CAPACITY = 8; private static final float DEFAULT_LOAD_FACTOR = 0.75f; @@ -47,45 +55,7 @@ public V put(long key, V value) { return currentBucket.put(key, value); } - private boolean isThresholdExceeded(long key) { - return !containsKey(key) - && (buckets.length == 0 || (int) (buckets.length * loadFactor) <= bucketCount); - } - - private void resize() { - if (buckets.length == ARRAY_MAX_SIZE) { - return; - } - - LongBucket[] newBuckets = new LongBucket[getNewSize()]; - for (LongBucket currentBucket : buckets) { - while (currentBucket != null) { - int newIndex = getIndex(currentBucket.getKey(), newBuckets); - LongBucket collision = currentBucket.getCollision(); - currentBucket.setCollision(null); - - //possible collision - LongBucket rehashedBucket = newBuckets[newIndex]; - if (rehashedBucket == null) { - newBuckets[newIndex] = currentBucket; - } else { - rehashedBucket.collide(currentBucket); - } - currentBucket = collision; - } - } - this.buckets = newBuckets; - } - - int getNewSize() { - if (buckets.length == 0) { - return DEFAULT_CAPACITY; - } - return ARRAY_MAX_SIZE / RESIZE_FACTOR > buckets.length - ? buckets.length * RESIZE_FACTOR - : ARRAY_MAX_SIZE; - } - + @Override public V get(long key) { int index = getIndex(key); return buckets[index] == null @@ -93,6 +63,7 @@ public V get(long key) { : buckets[index].get(key); } + @Override public V remove(long key) { int index = getIndex(key); @@ -110,14 +81,17 @@ public V remove(long key) { return null; } + @Override public boolean isEmpty() { return bucketCount == 0; } + @Override public boolean containsKey(long key) { return get(key) != null; } + @Override public boolean containsValue(V value) { for (LongBucket bucket : this) { if (bucket.getValue() == value || (bucket.getValue() != null && bucket.getValue().equals(value))) { @@ -127,62 +101,107 @@ public boolean containsValue(V value) { return false; } - public long[] keys() { + @Override + public V[] values() { if (bucketCount == 0) { - //just to stay consistent with values() + //impossible to instantiate a generic array since element type is not known at runtime return null; } - long[] keys = new long[bucketCount]; - int keyIndex = bucketCount; + V[] values = (V[]) Array.newInstance(this.iterator().next().getValue().getClass(), bucketCount); + int valueIndex = bucketCount; for (LongBucket bucket : this) { - keys[--keyIndex] = bucket.getKey(); + values[--valueIndex] = bucket.getValue(); } - return keys; + return values; } - public V[] values() { + @Override + public long[] keys() { if (bucketCount == 0) { - //impossible to instantiate a generic array since element type is not known at runtime + //just to stay consistent with values() return null; } - V[] values = (V[]) Array.newInstance(this.iterator().next().getValue().getClass(), bucketCount); - int valueIndex = bucketCount; + long[] keys = new long[bucketCount]; + int keyIndex = bucketCount; for (LongBucket bucket : this) { - values[--valueIndex] = bucket.getValue(); + keys[--keyIndex] = bucket.getKey(); } - return values; + return keys; } + @Override public long size() { return bucketCount; } + @Override public void clear() { Arrays.fill(buckets, null); bucketCount = 0; } + @Override + public Iterator> iterator() { + return new CollisionAwareLongBucketIterator(); + } + + private void resize() { + if (buckets.length == ARRAY_MAX_SIZE) { + return; + } + + LongBucket[] newBuckets = new LongBucket[getNewSize()]; + for (LongBucket currentBucket : buckets) { + while (currentBucket != null) { + int newIndex = getIndex(currentBucket.getKey(), newBuckets); + LongBucket collision = currentBucket.getCollision(); + currentBucket.setCollision(null); + + //possible collision + LongBucket rehashedBucket = newBuckets[newIndex]; + if (rehashedBucket == null) { + newBuckets[newIndex] = currentBucket; + } else { + rehashedBucket.collide(currentBucket); + } + currentBucket = collision; + } + } + this.buckets = newBuckets; + } + private int getIndex(long key) { return getIndex(key, buckets); } - private int getIndex(long key, LongBucket[] storage) { - return (Long.hashCode(key) & 0x7FFFFFFF) % storage.length; + private int getNewSize() { + if (buckets.length == 0) { + return DEFAULT_CAPACITY; + } + return ARRAY_MAX_SIZE / RESIZE_FACTOR > buckets.length + ? buckets.length * RESIZE_FACTOR + : ARRAY_MAX_SIZE; } - int getCapacity() { - return buckets.length; + private boolean isThresholdExceeded(long key) { + return !containsKey(key) + && (buckets.length == 0 || (int) (buckets.length * loadFactor) <= bucketCount); } - @Override - public Iterator> iterator() { - return new CollisionAwareLongBucketIterator(); + private int getIndex(long key, LongBucket[] storage) { + return (Long.hashCode(key) & 0x7FFFFFFF) % storage.length; } + /** + * A collision aware iterator which navigates either through bucket storage + * or down the collision linked list if available. + * It does not track concurrent modifications, so in case of concurrent storage modification it + * may produce unexpected results. + */ class CollisionAwareLongBucketIterator implements Iterator> { + private LongBucket next; private int currentIndex = 0; - public CollisionAwareLongBucketIterator() { next = getNext(); } @@ -216,13 +235,18 @@ private LongBucket getNext() { } return null; } + } + /** + * Represents a key-value pair. + * Also keeps a link to downstream collision if any and delegates to it in case of key mismatch for e.g. put(), get() operations + */ class LongBucket { + private final long key; private T value; private LongBucket collision; - private LongBucket(long key, T value) { this.key = key; this.value = value; @@ -236,18 +260,20 @@ public final T getValue() { return value; } + public final void setValue(T newValue) { + this.value = newValue; + } + + @Override public final String toString() { return key + "=" + value; } + @Override public final int hashCode() { return Objects.hash(key, value); } - public final void setValue(T newValue) { - this.value = newValue; - } - public LongBucket getCollision() { return collision; } @@ -296,13 +322,21 @@ public T put(long key, T value) { return null; } - public T collide(LongBucket anotherBucket) { + /** + * Convenience method for resizing and rehashing purposes + * @param anotherBucket - collision + */ + public void collide(LongBucket anotherBucket) { if (collision != null) { - return collision.collide(anotherBucket); + collision.collide(anotherBucket); + } else { + collision = anotherBucket; } - - collision = anotherBucket; - return null; } } + + /*package private for testing purposes*/ + int getCapacity() { + return buckets.length; + } } diff --git a/src/test/java/de/comparus/opensource/longmap/LongMapImplBasicTest.java b/src/test/java/de/comparus/opensource/longmap/LongMapImplBasicTest.java index e5fa0fa..6680c91 100644 --- a/src/test/java/de/comparus/opensource/longmap/LongMapImplBasicTest.java +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplBasicTest.java @@ -146,6 +146,15 @@ public void when_contains_value_then_return_true_otherwise_false() { assertFalse(instance.containsValue(valueTwo)); } + @Test + public void when_contains_null_value_then_return_true() { + instance.put(100L, null); + assertTrue(instance.containsValue(null)); + + instance.put(100L, "val"); + assertFalse(instance.containsValue(null)); + } + @Test public void when_get_keys_then_return_correct_keys_array() { From a497468266c01a1473e224a8bee1770f787255da Mon Sep 17 00:00:00 2001 From: psavin Date: Sat, 6 May 2023 15:08:36 +0300 Subject: [PATCH 5/5] Minor adjustments --- .../opensource/longmap/LongMapImpl.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index b3689de..83f83b1 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -202,6 +202,7 @@ class CollisionAwareLongBucketIterator implements Iterator> { private LongBucket next; private int currentIndex = 0; + public CollisionAwareLongBucketIterator() { next = getNext(); } @@ -247,6 +248,7 @@ class LongBucket { private final long key; private T value; private LongBucket collision; + private LongBucket(long key, T value) { this.key = key; this.value = value; @@ -283,17 +285,18 @@ public void setCollision(LongBucket collision) { } public T remove(long key) { - if (collision != null) { - if (collision.getKey() == key) { - T removed = collision.getValue(); - collision = collision.getCollision(); - bucketCount--; - return removed; - } else { - return collision.remove(key); - } + if (collision == null) { + return null; + } + + if (collision.getKey() == key) { + T removed = collision.getValue(); + collision = collision.getCollision(); + bucketCount--; + return removed; + } else { + return collision.remove(key); } - return null; } public T get(long key) { @@ -324,6 +327,7 @@ public T put(long key, T value) { /** * Convenience method for resizing and rehashing purposes + * * @param anotherBucket - collision */ public void collide(LongBucket anotherBucket) {