diff --git a/pom.xml b/pom.xml index 36c092b..0373da4 100644 --- a/pom.xml +++ b/pom.xml @@ -26,11 +26,24 @@ + + org.apache.commons + commons-lang3 + 3.13.0 + + junit junit - 4.12 + 4.13.2 test + + + org.projectlombok + lombok + 1.18.28 + provided + diff --git a/src/main/java/de/comparus/opensource/longmap/LongMap.java b/src/main/java/de/comparus/opensource/longmap/LongMap.java index adbf242..11c2ab1 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMap.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMap.java @@ -1,17 +1,82 @@ package de.comparus.opensource.longmap; +/** + * A map with keys of type long. + * + * @param the type of values stored in the map + */ public interface LongMap { + /** + * Associates the specified value with the specified key in this map. + * + * @param key the key with which the specified value is to be associated + * @param value the value to be associated with the specified key + * @return the previous value associated with the key, or null if there was no mapping for the key + */ V put(long key, V value); + + /** + * Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key. + * + * @param key the key whose associated value is to be returned + * @return the value to which the specified key is mapped, or null if this map contains no mapping for the key + */ V get(long key); + + /** + * Removes the mapping for the specified key from this map if present. + * + * @param key the key whose mapping is to be removed from the map + * @return the previous value associated with the key, or null if there was no mapping for the key + */ V remove(long key); + /** + * Returns true if this map contains no key-value mappings. + * + * @return true if this map contains no key-value mappings + */ boolean isEmpty(); + + /** + * Returns true if this map contains a mapping for the specified key. + * + * @param key the key whose presence in this map is to be tested + * @return true if this map contains a mapping for the specified key + */ boolean containsKey(long key); + + /** + * Returns true if this map contains at least one mapping with the specified value. + * + * @param value the value whose presence in this map is to be tested + * @return true if this map contains at least one mapping with the specified value + */ boolean containsValue(V value); + /** + * Returns an array containing all the keys in this map. + * + * @return an array containing all the keys in this map + */ long[] keys(); + + /** + * Returns an array containing all the values in this map. + * + * @return an array containing all the values in this map + */ V[] values(); + /** + * Returns the number of key-value mappings in this map. + * + * @return the number of key-value mappings in this map + */ long size(); + + /** + * Removes all of the mappings from this map. The map will be empty after this call returns. + */ void clear(); } diff --git a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java index 2f0b54b..4b60d71 100644 --- a/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java +++ b/src/main/java/de/comparus/opensource/longmap/LongMapImpl.java @@ -1,43 +1,183 @@ package de.comparus.opensource.longmap; +import lombok.AllArgsConstructor; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + public class LongMapImpl implements LongMap { + + private static final int CAPACITY_LEVEL = 2; + private static final int DEFAULT_CAPACITY = 16; + private static final double LOAD_FACTOR = 0.75; + + private Entry[] table; + private int size; + + public LongMapImpl() { + this(DEFAULT_CAPACITY); + } + + public LongMapImpl(int initialCapacity) { + if (initialCapacity <= 0) { + throw new IllegalArgumentException("Initial capacity must be positive"); + } + table = new Entry[initialCapacity]; + size = 0; + } + + @Override public V put(long key, V value) { - return null; + if (size >= table.length * LOAD_FACTOR) { + resize(); + } + int index = hash(key) % table.length; + + Entry entry = table[index]; + while (entry != null) { + if (entry.key == key) { + entry.value = value; + return value; + } + entry = entry.next; + } + + table[index] = new Entry<>(key, value, table[index]); + size++; + return value; } + @Override public V get(long key) { + int index = hash(key) % table.length; + Entry entry = table[index]; + while (entry != null) { + if (entry.key == key) { + return entry.value; + } + entry = entry.next; + } return null; } + @Override public V remove(long key) { - return null; + int index = hash(key) % table.length; + Entry prev = null; + Entry current = table[index]; + + while (current != null) { + if (current.key == key) { + if (prev == null) { + table[index] = current.next; + } else { + prev.next = current.next; + } + size--; + return current.value; + } + prev = current; + current = current.next; + } + return prev != null ? prev.value : null; } + @Override public boolean isEmpty() { - return false; + return size == 0; } + @Override public boolean containsKey(long key) { + int index = hash(key) % table.length; + Entry entry = table[index]; + while (entry != null) { + if (entry.key == key) { + return true; + } + entry = entry.next; + } return false; } + @Override public boolean containsValue(V value) { + for (Entry entry : table) { + while (entry != null) { + if (Objects.equals(entry.value, value)) { + return true; + } + entry = entry.next; + } + } return false; } + @Override public long[] keys() { - return null; + long[] keys = new long[(int) size()]; + int index = 0; + for (Entry entry : table) { + while (entry != null) { + keys[index++] = entry.key; + entry = entry.next; + } + } + return keys; } + @Override public V[] values() { - return null; + List valueList = new ArrayList<>(); + for (Entry entry : table) { + while (entry != null) { + valueList.add(entry.value); + entry = entry.next; + } + } + return valueList.toArray((V[]) Array.newInstance(valueList.get(0).getClass(), valueList.size())); } + @Override public long size() { - return 0; + return size; } + @Override public void clear() { + if ((table) != null && size > 0) { + size = 0; + Arrays.fill(table, null); + } + } + + private int hash(long key) { + return Long.hashCode(key); + } + + private void resize() { + int newCapacity = table.length * CAPACITY_LEVEL; + Entry[] newTable = new Entry[newCapacity]; + + for (Entry entry : table) { + while (entry != null) { + int index = hash(entry.key) % newCapacity; + Entry next = entry.next; + entry.next = newTable[index]; + newTable[index] = entry; + entry = next; + } + } + table = newTable; + } + @AllArgsConstructor + private static class Entry { + long key; + V value; + Entry next; } } diff --git a/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java new file mode 100644 index 0000000..ae52a57 --- /dev/null +++ b/src/test/java/de/comparus/opensource/longmap/LongMapImplTest.java @@ -0,0 +1,224 @@ +package de.comparus.opensource.longmap; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import static org.junit.Assert.*; + +/** + * @author deya + */ +@RunWith(DataProviderRunner.class) +public class LongMapImplTest { + private static final int NUM_OPERATIONS = 25; + private final LongMap map = new LongMapImpl<>(); + + @Before + public void setUp() { + map.clear(); + } + + @Test + @UseDataProvider("testGenericDataProvider") + public void testPutAndGet(Map initialMap) { + for (Map.Entry entry : initialMap.entrySet()) { + String putResult = map.put(entry.getKey(), entry.getValue()); + assertNotNull(putResult); + assertEquals(entry.getValue(), putResult); + + String getResult = map.get(entry.getKey()); + assertNotNull(getResult); + assertEquals(entry.getValue(), getResult); + } + + assertNull(map.get(generateKey())); + assertNull(null); + } + + @Test + public void testPutAndGetObject() { + LongMap objectLongMap = new LongMapImpl<>(); + + CustomObject initialObject = new CustomObject(generateKey()); + + long key = generateKey(); + CustomObject returnedValue = (CustomObject) objectLongMap.put(key, initialObject); + assertNotNull(returnedValue); + assertEquals(initialObject.getId(), returnedValue.getId()); + + CustomObject getResult = (CustomObject) objectLongMap.get(key); + assertNotNull(getResult); + assertEquals(initialObject.getId(), getResult.getId()); + } + + @Test + public void testPutAndGetCustomObject() { + LongMap customObjectLongMap = new LongMapImpl<>(); + + CustomObject initialObject = new CustomObject(generateKey()); + + long key = generateKey(); + CustomObject returnedValue = customObjectLongMap.put(key, initialObject); + assertNotNull(returnedValue); + assertEquals(initialObject.getId(), returnedValue.getId()); + + CustomObject getResult = customObjectLongMap.get(key); + assertNotNull(getResult); + assertEquals(initialObject.getId(), getResult.getId()); + } + + @Test + public void testPutDuplicateKey() { + long key = generateKey(); + String value = RandomStringUtils.randomAscii(10); + + map.put(key, RandomStringUtils.randomAscii(10)); + map.put(key, value); + + assertEquals(value, map.get(key)); + } + + @Test + @UseDataProvider("testGenericDataProvider") + public void testRemove(Map initialMap) { + for (Map.Entry entry : initialMap.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + + int mapSize = initialMap.size(); + for (Map.Entry entry : initialMap.entrySet()) { + String removedValue = map.remove(entry.getKey()); + assertNotNull(removedValue); + assertEquals(entry.getValue(), removedValue); + assertNull(map.get(entry.getKey())); + assertEquals(--mapSize, map.size()); + } + } + + @Test + @UseDataProvider("testGenericDataProvider") + public void testIsEmpty(Map initialMap) { + assertTrue(map.isEmpty()); + for (Map.Entry entry : initialMap.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + assertFalse(map.isEmpty()); + } + + @Test + @UseDataProvider("testGenericDataProvider") + public void testContainsKey(Map initialMap) { + for (Map.Entry entry : initialMap.entrySet()) { + assertFalse(map.containsKey(entry.getKey())); + } + for (Map.Entry entry : initialMap.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + for (Map.Entry entry : initialMap.entrySet()) { + assertTrue(map.containsKey(entry.getKey())); + } + } + + @Test + @UseDataProvider("testGenericDataProvider") + public void testContainsValue(Map initialMap) { + for (Map.Entry entry : initialMap.entrySet()) { + assertFalse(map.containsValue(entry.getValue())); + } + + for (Map.Entry entry : initialMap.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + for (Map.Entry entry : initialMap.entrySet()) { + assertTrue(map.containsValue(entry.getValue())); + } + } + + @DataProvider + public static Object[][] testGenericDataProvider() { + Map somePrefilledHashMap = new HashMap<>(); + for (long i = 0; i < NUM_OPERATIONS; i++) { + somePrefilledHashMap.put(i, RandomStringUtils.randomAscii(NUM_OPERATIONS)); + } + + return new Object[][]{ + {somePrefilledHashMap} + }; + } + + @Test + @UseDataProvider("testGenericDataProvider") + public void testKeys(Map initialMap) { + for (Map.Entry entry : initialMap.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + assertEquals(initialMap.size(), map.size()); + + List keysList = LongStream.of(map.keys()) + .boxed() + .collect(Collectors.toList()); + for (Map.Entry entry : initialMap.entrySet()) { + assertTrue(keysList.contains(entry.getKey())); + } + } + + @Test + @UseDataProvider("testGenericDataProvider") + public void testValues(Map initialMap) { + for (Map.Entry entry : initialMap.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + assertEquals(initialMap.size(), map.size()); + + String[] mapValues = map.values(); + List valuesList = Arrays.stream(mapValues). + collect(Collectors.toList()); + for (Map.Entry entry : initialMap.entrySet()) { + assertTrue(valuesList.contains(entry.getValue())); + } + } + + @Test + public void testSize() { + assertEquals(0, map.size()); + for (long i = 0; i < NUM_OPERATIONS; i++) { + map.put(i, RandomStringUtils.random(NUM_OPERATIONS)); + } + assertEquals(NUM_OPERATIONS, map.size()); + } + + @Test + public void testClear() { + for (long i = 0; i < NUM_OPERATIONS; i++) { + map.put(i, RandomStringUtils.random(NUM_OPERATIONS)); + } + assertEquals(NUM_OPERATIONS, map.size()); + + map.clear(); + assertEquals(0, map.size()); + } + + private long generateKey() { + return Long.parseLong(RandomStringUtils.randomNumeric(3)); + } + + @Getter + @AllArgsConstructor + private static class CustomObject { + private long id; + } +} \ No newline at end of file diff --git a/src/test/java/de/comparus/opensource/longmap/LongMapPerformanceTest.java b/src/test/java/de/comparus/opensource/longmap/LongMapPerformanceTest.java new file mode 100644 index 0000000..3c6a061 --- /dev/null +++ b/src/test/java/de/comparus/opensource/longmap/LongMapPerformanceTest.java @@ -0,0 +1,63 @@ +package de.comparus.opensource.longmap; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertNotNull; + +/** + * Just a demo to check performance, may be extended according to the common practice. + * Example of evaluation results: + * Performed 10000000 put and get operations in with longMap is 23.216132846 seconds + * Performed 10000000 put and get operations in with hashMap is 24.068466414 seconds + * The difference is 3.5412873979549384 % + * + * Performed 100000 put and get operations in with longMap is 0.593142796 seconds + * Performed 100000 put and get operations in with hashMap is 0.397493066 seconds + * The difference is 32.985266165147856 % + * @author deya + */ +public class LongMapPerformanceTest { + private static final int NUM_OPERATIONS = 10_000_000; + + @Test + public void testPutAndGetPerformance() { + long startTime = System.nanoTime(); + LongMapImpl longMap = new LongMapImpl<>(); + for (long i = 0; i < NUM_OPERATIONS; i++) { + longMap.put(i, RandomStringUtils.random(10)); + } + + for (long i = 0; i < NUM_OPERATIONS; i++) { + longMap.get(i); + } + long endTime = System.nanoTime(); + long elapsedTime = endTime - startTime; + double longMapDuration = elapsedTime / 1_000_000_000.0; + + startTime = System.nanoTime(); + Map hashMap = new HashMap<>(); + for (long i = 0; i < NUM_OPERATIONS; i++) { + hashMap.put(i, RandomStringUtils.random(10)); + } + + for (long i = 0; i < NUM_OPERATIONS; i++) { + String value = hashMap.get(i); + assertNotNull(value); + } + endTime = System.nanoTime(); + elapsedTime = endTime - startTime; + double hashMapDuration = elapsedTime / 1_000_000_000.0; + + double difference = Math.abs(hashMapDuration - longMapDuration); + double percentDifference = (difference / Math.max(hashMapDuration, longMapDuration)) * 100.0; + + System.out.printf("Performed %s put and get operations in with longMap is %s seconds", NUM_OPERATIONS, longMapDuration); + System.out.println(); + System.out.printf("Performed %s put and get operations in with hashMap is %s seconds", NUM_OPERATIONS, hashMapDuration); + System.out.printf("The difference is %s %%", percentDifference); + } +}