diff --git a/pom.xml b/pom.xml
index 36c092b..a4f049e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,9 +27,9 @@
- junit
- junit
- 4.12
+ org.junit.jupiter
+ junit-jupiter
+ RELEASE
test
diff --git a/src/main/java/de/comparus/opensource/longmap/CollisionAwareLongBucketIterator.java b/src/main/java/de/comparus/opensource/longmap/CollisionAwareLongBucketIterator.java
new file mode 100644
index 0000000..c856ce6
--- /dev/null
+++ b/src/main/java/de/comparus/opensource/longmap/CollisionAwareLongBucketIterator.java
@@ -0,0 +1,46 @@
+package de.comparus.opensource.longmap;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+class CollisionAwareLongBucketIterator implements Iterator> {
+
+ private final LongMapNode[] buckets;
+ private LongMapNode next;
+ private int currentIndex = 0;
+
+ public CollisionAwareLongBucketIterator(LongMapNode[] buckets) {
+ this.buckets = buckets;
+ next = getNext();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return next != null;
+ }
+
+ @Override
+ public LongMapNode next() {
+ if (next == null) {
+ throw new NoSuchElementException("There is no next element to iterate upon.");
+ }
+
+ LongMapNode current = next;
+ next = getNext();
+ return current;
+ }
+
+ private LongMapNode getNext() {
+ if (next != null && next.getCollision() != null) {
+ return next.getCollision();
+ }
+
+ while (currentIndex < buckets.length) {
+ if (buckets[currentIndex] != null) {
+ return buckets[currentIndex++];
+ }
+ currentIndex++;
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java
index 2f0b54b..878c429 100644
--- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java
+++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java
@@ -1,43 +1,191 @@
package de.comparus.opensource.longmap;
-public class LongMapImpl implements LongMap {
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Iterator;
+
+public class LongMapImpl implements LongMap, Iterable> {
+ 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 LongMapNode[] buckets;
+
+ public LongMapImpl() {
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
+ }
+
+ @SuppressWarnings("unchecked")
+ public LongMapImpl(int capacity, float loadFactor) {
+ this.loadFactor = loadFactor;
+ this.buckets = new LongMapNode[capacity];
+ }
+
public V put(long key, V value) {
- return null;
+ if (bucketCount == ARRAY_MAX_SIZE) {
+ throw new IllegalStateException(String.format(
+ "Instance of Map is capped. It can only store %s mappings at max.", ARRAY_MAX_SIZE
+ ));
+ }
+
+ if (isThresholdExceeded(key)) {
+ resize();
+ }
+
+ int bucketIndex = getIndex(key);
+ LongMapNode currentBucket = buckets[bucketIndex];
+
+ if (currentBucket == null) {
+ buckets[bucketIndex] = new LongMapNode<>(key, value);
+ bucketCount++;
+ return null;
+ }
+
+ V storedValue = currentBucket.put(key, value);
+
+ if (storedValue == null) {
+ bucketCount++;
+ }
+ return storedValue;
}
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);
+ LongMapNode currentBucket = buckets[index];
+
+ if (currentBucket != null) {
+ if (currentBucket.getKey() == key) {
+ V result = currentBucket.getValue();
+ buckets[index] = currentBucket.getCollision();
+ bucketCount--;
+ return result;
+ } else {
+ V removedValue = currentBucket.remove(key);
+
+ if (removedValue != null) {
+ bucketCount--;
+ }
+
+ return removedValue;
+ }
+ }
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 (LongMapNode 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) {
+ return null;
+ }
+ long[] keys = new long[bucketCount];
+ int keyIndex = bucketCount;
+ for (LongMapNode bucket : this) {
+ keys[--keyIndex] = bucket.getKey();
+ }
+ return keys;
}
+ @SuppressWarnings("unchecked")
public V[] values() {
- return null;
+ if (bucketCount == 0) {
+ return null;
+ }
+ V[] values = (V[]) Array.newInstance(this.iterator().next().getValue().getClass(), bucketCount);
+ int valueIndex = bucketCount;
+ for (LongMapNode bucket : this) {
+ values[--valueIndex] = bucket.getValue();
+ }
+ return values;
}
public long size() {
- return 0;
+ return bucketCount;
}
public void clear() {
+ Arrays.fill(buckets, null);
+ bucketCount = 0;
+ }
+
+ @Override
+ public Iterator> iterator() {
+ return new CollisionAwareLongBucketIterator<>(buckets);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void resize() {
+ if (buckets.length == ARRAY_MAX_SIZE) {
+ return;
+ }
+
+ LongMapNode[] newBuckets = new LongMapNode[getNewSize()];
+ for (LongMapNode currentBucket : buckets) {
+ while (currentBucket != null) {
+ int newIndex = getIndex(currentBucket.getKey(), newBuckets);
+ LongMapNode collision = currentBucket.getCollision();
+ currentBucket.setCollision(null);
+
+ LongMapNode 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 getNewSize() {
+ if (buckets.length == 0) {
+ return DEFAULT_CAPACITY;
+ }
+ return ARRAY_MAX_SIZE / RESIZE_FACTOR > buckets.length
+ ? buckets.length * RESIZE_FACTOR
+ : ARRAY_MAX_SIZE;
+ }
+
+ private boolean isThresholdExceeded(long key) {
+ return !containsKey(key)
+ && (buckets.length == 0 || (int) (buckets.length * loadFactor) <= bucketCount);
+ }
+
+ private int getIndex(long key, LongMapNode[] storage) {
+ return (Long.hashCode(key) & 0x7FFFFFFF) % storage.length;
+ }
+ public int getCapacity() {
+ return buckets.length;
}
}
diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapNode.java b/src/main/java/de/comparus/opensource/longmap/LongMapNode.java
new file mode 100644
index 0000000..74f6c70
--- /dev/null
+++ b/src/main/java/de/comparus/opensource/longmap/LongMapNode.java
@@ -0,0 +1,92 @@
+package de.comparus.opensource.longmap;
+
+import java.util.Objects;
+
+class LongMapNode {
+
+ private final long key;
+ private T value;
+ private LongMapNode collision;
+
+ public LongMapNode(long key, T value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public final long getKey() {
+ return key;
+ }
+
+ 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 LongMapNode getCollision() {
+ return collision;
+ }
+
+ public void setCollision(LongMapNode collision) {
+ this.collision = collision;
+ }
+
+ public T remove(long key) {
+ if (collision == null) {
+ return null;
+ }
+
+ if (collision.getKey() == key) {
+ T removed = collision.getValue();
+ collision = collision.getCollision();
+ return removed;
+ } else {
+ return collision.remove(key);
+ }
+ }
+
+ 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 LongMapNode<>(key, value);
+ return null;
+ }
+
+ public void collide(LongMapNode anotherBucket) {
+ if (collision != null) {
+ collision.collide(anotherBucket);
+ } else {
+ collision = anotherBucket;
+ }
+ }
+}
diff --git a/src/test/java/LongMapImplTest.java b/src/test/java/LongMapImplTest.java
new file mode 100644
index 0000000..8ec077b
--- /dev/null
+++ b/src/test/java/LongMapImplTest.java
@@ -0,0 +1,290 @@
+import de.comparus.opensource.longmap.LongMap;
+import de.comparus.opensource.longmap.LongMapImpl;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.logging.Logger;
+import java.util.stream.LongStream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(TimingExtension.class)
+class LongMapImplTest {
+ private static final Logger LOGGER = Logger.getLogger(TimingExtension.class.getName());
+ private static final String PREFIX = "value_";
+ private final LongMapImpl stringLongMap = new LongMapImpl<>();
+ private LongMap integerLongMap;
+
+
+ @RepeatedTest(10)
+ public void put_new_then_store_correctly() {
+ putRandom(100);
+
+ int expectedSize = 100;
+ assertEquals(expectedSize, stringLongMap.size());
+
+ int expectedCapacity = 256;
+ assertEquals(expectedCapacity, stringLongMap.getCapacity());
+ }
+
+ @Test
+ public void put_existing_then_update_correctly() {
+ int mappingsNumber = 100;
+ long[] keys = putRandom(mappingsNumber);
+ Arrays.stream(keys)
+ .forEach(key -> {
+ String newValue = PREFIX + key;
+ String oldValue = stringLongMap.put(key, newValue);
+ String updatedValue = stringLongMap.get(key);
+
+ assertEquals(PREFIX + key, oldValue);
+ assertEquals(newValue, updatedValue);
+ });
+
+ assertEquals(mappingsNumber, stringLongMap.size());
+
+ int expectedCapacity = 256;
+ assertEquals(expectedCapacity, stringLongMap.getCapacity());
+ }
+
+ @RepeatedTest(10)
+ public void get_then_return_correctly() {
+ long[] keys = putRandom(100);
+
+ Arrays.stream(keys)
+ .forEach(key -> {
+ String value = stringLongMap.get(key);
+ assertNotNull(value);
+ assertEquals(PREFIX + key, value);
+ });
+ }
+
+ @Test
+ public void get_and_map_is_empty_then_return_null() {
+ assertNull(stringLongMap.get(100L));
+ }
+
+ @Test
+ public void get_with_wrong_key_then_return_null() {
+ put(6);
+ assertNull(stringLongMap.get(7));
+ }
+
+ @Test
+ public void 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 = stringLongMap.remove(key);
+ assertNotNull(removed);
+ assertEquals(PREFIX + key, removed);
+ assertNull(stringLongMap.get(key));
+ });
+
+ assertEquals(mappingsNumber - toBeRemoved, stringLongMap.size());
+ }
+
+ @Test
+ public void remove_with_wrong_key_then_no_delete() {
+ put(6);
+ String removed = stringLongMap.remove(10);
+
+ assertNull(removed);
+ assertEquals(6, stringLongMap.size());
+ }
+
+ @Test
+ public void empty_then_return_true_otherwise_false() {
+ assertTrue(stringLongMap.isEmpty());
+
+ put(2);
+ assertFalse(stringLongMap.isEmpty());
+
+ stringLongMap.remove(0);
+ stringLongMap.remove(1);
+ assertTrue(stringLongMap.isEmpty());
+
+ put(2);
+ stringLongMap.clear();
+ assertTrue(stringLongMap.isEmpty());
+
+ put(2);
+ stringLongMap.remove(0);
+ assertFalse(stringLongMap.isEmpty());
+ }
+
+ @Test
+ public void contains_key_then_return_true_otherwise_false() {
+ long[] keys = putRandom(10);
+ assertTrue(stringLongMap.containsKey(keys[5]));
+
+ stringLongMap.remove(keys[5]);
+ assertFalse(stringLongMap.containsKey(keys[5]));
+
+ stringLongMap.clear();
+ assertFalse(stringLongMap.containsKey(keys[5]));
+ }
+
+ @Test
+ public void contains_value_then_return_true_otherwise_false() {
+ long[] keys = putRandom(10);
+ String valueOne = PREFIX + keys[3];
+ String valueTwo = PREFIX + keys[7];
+ assertTrue(stringLongMap.containsValue(valueOne));
+ assertTrue(stringLongMap.containsValue(valueTwo));
+ assertFalse(stringLongMap.containsValue(PREFIX + "wrong"));
+
+ stringLongMap.remove(keys[3]);
+ assertFalse(stringLongMap.containsValue(valueOne));
+
+ stringLongMap.clear();
+ assertFalse(stringLongMap.containsValue(valueTwo));
+ }
+
+ @Test
+ public void contains_null_value_then_return_true() {
+ stringLongMap.put(100L, null);
+ assertTrue(stringLongMap.containsValue(null));
+
+ stringLongMap.put(100L, "val");
+ assertFalse(stringLongMap.containsValue(null));
+ }
+
+ @Test
+ public void get_keys_then_return_correct_keys_array() {
+ long[] expectedKeys = Arrays.stream(putRandom(40)).sorted().toArray();
+ long[] actualKeys = Arrays.stream(stringLongMap.keys()).sorted().toArray();
+ assertEquals(stringLongMap.size(), actualKeys.length);
+ assertArrayEquals(expectedKeys, actualKeys);
+
+ stringLongMap.remove(expectedKeys[10]);
+ stringLongMap.remove(expectedKeys[25]);
+
+ long[] actualUpdatedKeys = Arrays.stream(stringLongMap.keys()).sorted().toArray();
+ long[] expectedUpdatedKeys = Arrays.stream(expectedKeys)
+ .filter(key -> key != expectedKeys[10] && key != expectedKeys[25])
+ .sorted()
+ .toArray();
+ assertEquals(stringLongMap.size(), expectedUpdatedKeys.length);
+ assertArrayEquals(expectedUpdatedKeys, actualUpdatedKeys);
+
+ stringLongMap.clear();
+ long[] emptyKeys = stringLongMap.keys();
+ assertNull(emptyKeys);
+ }
+
+ @Test
+ public void get_values_then_return_correct_value_array() {
+ int mappingsNumber = 20;
+ long[] keys = putRandom(mappingsNumber);
+
+ String[] actualValues = Arrays.stream(stringLongMap.values()).sorted().toArray(String[]::new);
+ String[] expectedValues = Arrays.stream(keys)
+ .mapToObj(key -> PREFIX + key)
+ .sorted()
+ .toArray(String[]::new);
+ assertEquals(stringLongMap.size(), actualValues.length);
+ assertArrayEquals(expectedValues, actualValues);
+
+ stringLongMap.remove(keys[11]);
+ stringLongMap.remove(keys[19]);
+
+ String[] actualUpdatedValues = Arrays.stream(stringLongMap.values()).sorted().toArray(String[]::new);
+ String[] expectedUpdatedValues = Arrays.stream(keys)
+ .filter(key -> key != keys[11] && key != keys[19])
+ .mapToObj(key -> PREFIX + key)
+ .sorted()
+ .toArray(String[]::new);
+ assertEquals(actualUpdatedValues.length, expectedUpdatedValues.length);
+ assertEquals(stringLongMap.size(), actualUpdatedValues.length);
+ assertArrayEquals(expectedUpdatedValues, actualUpdatedValues);
+
+ stringLongMap.clear();
+ String[] emptyValues = stringLongMap.values();
+ assertNull(emptyValues);
+ }
+
+ @Test
+ public void get_size_return_correct_value() {
+ assertEquals(0, stringLongMap.size());
+
+ long[] keys = putRandom(100);
+ assertEquals(100, stringLongMap.size());
+
+ stringLongMap.remove(keys[2]);
+ stringLongMap.remove(keys[10]);
+ stringLongMap.remove(keys[59]);
+ assertEquals(97, stringLongMap.size());
+
+ stringLongMap.clear();
+ assertEquals(0, stringLongMap.size());
+ }
+
+
+ private void put(long mappingsNumber) {
+ LongStream
+ .range(0L, mappingsNumber)
+ .forEach(key -> stringLongMap.put(key, PREFIX + key));
+ }
+
+ private long[] putRandom(long mappingsNumber) {
+ Random random = new Random();
+ return LongStream
+ .range(0L, mappingsNumber)
+ .map(l -> random.nextLong())
+ .peek(key -> stringLongMap.put(key, PREFIX + key))
+ .toArray();
+ }
+
+ @Test
+ void putMemoryUsage() {
+ integerLongMap = new LongMapImpl<>();
+
+ Runtime runtime = Runtime.getRuntime();
+ long memoryBeforeFillingMap = runtime.totalMemory() - runtime.freeMemory();
+
+ fillMap(10000);
+
+ long memoryAfterFillingMap = runtime.totalMemory() - runtime.freeMemory();
+ LOGGER.info(() ->
+ String.format("Filling LongMap took %s bytes", memoryBeforeFillingMap - memoryAfterFillingMap));
+ }
+
+ @Test
+ void putJavaHashMapMemoryUsage() {
+ Map example = new HashMap<>();
+ Runtime runtime = Runtime.getRuntime();
+ long memoryBeforeFillingMap = runtime.totalMemory() - runtime.freeMemory();
+
+ fillJavaMap(10000, example);
+ long memoryAfterFillingMap = runtime.totalMemory() - runtime.freeMemory();
+ LOGGER.info(() ->
+ String.format("Filling HashMap took %s bytes", memoryBeforeFillingMap - memoryAfterFillingMap));
+ }
+
+ @Test
+ void empty_bucket_should_return_null() {
+ integerLongMap = new LongMapImpl<>();
+
+ fillMap(10);
+
+ assertNull(integerLongMap.get(2473248623L));
+ }
+
+ private void fillMap(int amount) {
+ TestUtil.apply(amount, TestUtil.BOUND, (key, value) -> integerLongMap.put(key, value));
+ }
+
+ private void fillJavaMap(int amount, Map example) {
+ TestUtil.apply(amount, TestUtil.BOUND, example::put);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/TestUtil.java b/src/test/java/TestUtil.java
new file mode 100644
index 0000000..2c1bcd5
--- /dev/null
+++ b/src/test/java/TestUtil.java
@@ -0,0 +1,16 @@
+import java.util.Random;
+import java.util.function.BiFunction;
+
+public class TestUtil {
+ public static final int BOUND = 32000;
+ private static final Random RANDOM = new Random();
+
+ public static void apply(int amount, int bound, BiFunction operator) {
+ for (int i = 0; i < amount; i++) {
+ long key = RANDOM.nextLong();
+ int value = RANDOM.nextInt(bound);
+
+ operator.apply(key, value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/TimingExtension.java b/src/test/java/TimingExtension.java
new file mode 100644
index 0000000..b2da010
--- /dev/null
+++ b/src/test/java/TimingExtension.java
@@ -0,0 +1,34 @@
+
+import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
+import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import java.lang.reflect.Method;
+import java.util.logging.Logger;
+
+public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
+
+ private static final Logger LOGGER = Logger.getLogger(TimingExtension.class.getName());
+
+ private static final String START_TIME = "start time";
+
+ @Override
+ public void beforeTestExecution(ExtensionContext context) {
+ getStore(context).put(START_TIME, System.currentTimeMillis());
+ }
+
+ @Override
+ public void afterTestExecution(ExtensionContext context) {
+ Method testMethod = context.getRequiredTestMethod();
+ long startTime = getStore(context).remove(START_TIME, long.class);
+ long duration = System.currentTimeMillis() - startTime;
+
+ LOGGER.info(() ->
+ String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
+ }
+
+ private ExtensionContext.Store getStore(ExtensionContext context) {
+ return context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod()));
+ }
+
+}
\ No newline at end of file