diff --git a/README.md b/README.md index 1b1621c..b891397 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Special purpose collections for Java -[![Java 7+](https://img.shields.io/badge/java-7+-4c7e9f.svg)](http://java.oracle.com) +[![Java 7+](https://img.shields.io/badge/java-8+-4c7e9f.svg)](http://java.oracle.com) [![License](https://img.shields.io/badge/license-MIT-4c7e9f.svg)](https://raw.githubusercontent.com/twineworks/collections/master/LICENSE.txt) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.twineworks/collections/badge.svg)](http://search.maven.org/#search|gav|1|g:"com.twineworks"%20AND%20a:"collections") [![Travis Build Status](https://travis-ci.org/twineworks/collections.svg?branch=master)](https://travis-ci.org/twineworks/collections) @@ -16,7 +16,7 @@ ShapeMaps are high performance maps similar to HashMaps. * They _do not_ allow `null` keys. * They _do_ allow `null` values. * Like HashMaps, they are not thread-safe. You need to provide your own synchronization. - * Compatible with Java 7 and above + * Compatible with Java 8 and above ShapeMaps implement a combination of the following performance optimization techniques: @@ -159,6 +159,27 @@ a single pass to fill and consume the whole batch. TrieLists are a persistent vector implementation based on a tree of 6 bit (64-item) Trie nodes with tail operation optimization. +## ChampMaps +Trie-based persistent hash maps. + + * They _do not_ allow `null` keys. + * They _do not_ allow `null` values. + +See [Michael Steindorfer's Phd. thesis](https://michael.steindorfer.name/publications/phd-thesis-efficient-immutable-collections.pdf) on the CHAMP data structure. +This implementation is based on the excellent implementation in the [capsule](https://github.com/usethesource/capsule) project. + +Notable changes are: + - mutable transients are not tied to a specific mutator thread, you can create deep copies of a transient to share with other threads via dup() + - equal values do not cause an update in compact bitmap nodes, the original item stays in the map + - set vs. put semantics in transients, previous value of a key is not returned + - entry iteration is using standard JDK [AbstractMap.SimpleEntry](https://docs.oracle.com/javase/8/docs/api/java/util/AbstractMap.SimpleEntry.html) + - transients implement remove() + - Always calls key.equals and value.equals for comparisons, custom equivalence tester is not supported + - minor changes removing 0-length array copies from the code + - recursive equality check on node array uses Arrays.equals() + - implementations of removeAll() for map and transient + - minor optimizations for implementations of setAll() + ## License This project uses the business friendly [MIT](https://opensource.org/licenses/MIT) license. diff --git a/src/main/java/com/twineworks/collections/champ/ChampEntry.java b/src/main/java/com/twineworks/collections/champ/ChampEntry.java new file mode 100644 index 0000000..fbb553c --- /dev/null +++ b/src/main/java/com/twineworks/collections/champ/ChampEntry.java @@ -0,0 +1,13 @@ +package com.twineworks.collections.champ; + +final public class ChampEntry { + + final public K key; + final public V value; + + public ChampEntry(K key, V value) { + this.key = key; + this.value = value; + } + +} diff --git a/src/main/java/com/twineworks/collections/champ/ChampMap.java b/src/main/java/com/twineworks/collections/champ/ChampMap.java new file mode 100644 index 0000000..c1142ad --- /dev/null +++ b/src/main/java/com/twineworks/collections/champ/ChampMap.java @@ -0,0 +1,431 @@ +package com.twineworks.collections.champ; + +import java.util.*; + +public class ChampMap { + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static final ChampNode EMPTY_NODE = new CompactBitmapNode(null, 0, 0, new Object[0]); + @SuppressWarnings({"unchecked", "rawtypes"}) + private static final ChampMap EMPTY_MAP = new ChampMap(EMPTY_NODE, 0, 0); + final ChampNode rootNode; + final int cachedHashCode; + final int cachedSize; + + public ChampMap(ChampNode rootNode, int cachedHashCode, int cachedSize) { + this.rootNode = rootNode; + this.cachedHashCode = cachedHashCode; + this.cachedSize = cachedSize; + } + + @SuppressWarnings("unchecked") + public static ChampMap empty() { + return ChampMap.EMPTY_MAP; + } + + public ChampMap set(K key, V value) { + final int keyHash = key.hashCode(); + final UpdateResult ur = UpdateResult.unchanged(); + + final ChampNode newRootNode = rootNode.update(null, key, value, keyHash, 0, ur); + + if (ur.isModified()) { + if (ur.hasReplacedValue()) { + final int valHashOld = ur.getReplacedValue().hashCode(); + final int valHashNew = value.hashCode(); + + return new ChampMap<>(newRootNode, + cachedHashCode + ((keyHash ^ valHashNew)) - ((keyHash ^ valHashOld)), cachedSize); + } + + final int valHash = value.hashCode(); + return new ChampMap<>(newRootNode, cachedHashCode + ((keyHash ^ valHash)), cachedSize + 1); + } + + return this; + } + + public ChampMap setAll(ChampMap m) { + TransientChampMap t = new TransientChampMap<>(this); + t.setAll(m); + return t.freeze(); + } + + public ChampMap setAll(Map m) { + TransientChampMap t = new TransientChampMap<>(this); + t.setAll(m.entrySet().iterator()); + return t.freeze(); + } + + public V get(K key) { + return rootNode.findByKey(key, key.hashCode(), 0); + } + + @SuppressWarnings("unchecked") + public boolean containsKey(Object key) { + return rootNode.containsKey((K) key, key.hashCode(), 0); + } + + public ChampMap remove(K key) { + + final int keyHash = key.hashCode(); + final UpdateResult ur = UpdateResult.unchanged(); + + final ChampNode newRootNode = rootNode.remove(null, key, + keyHash, 0, ur); + + if (ur.isModified()) { + final int valHash = ur.getReplacedValue().hashCode(); + return new ChampMap(newRootNode, cachedHashCode - ((keyHash ^ valHash)), + cachedSize - 1); + } + + return this; + } + + public ChampMap removeAll(Collection keys){ + + ChampNode newRootNode = rootNode; + int cachedHashCode = this.cachedHashCode; + int cachedSize = this.cachedSize; + UpdateResult ur = UpdateResult.unchanged(); + + for (K key : keys) { + final int keyHash = key.hashCode(); + + newRootNode = rootNode.remove(null, key, keyHash, 0, ur); + + if (ur.isModified()) { + final int valHash = ur.getReplacedValue().hashCode(); + cachedHashCode -= (keyHash ^ valHash); + cachedSize--; + ur.reset(); + } + } + + if (cachedSize == this.cachedSize){ + return this; + } + + return new ChampMap<>(newRootNode, cachedHashCode, cachedSize); + + } + + public int size() { + return cachedSize; + } + + @Override + public int hashCode() { + return cachedHashCode; + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object other) { + if (other == null) return false; + if (this == other) return true; + if (other.getClass() != this.getClass()) return false; + ChampMap otherMap = (ChampMap) other; + if (cachedSize != otherMap.cachedSize) return false; + if (cachedHashCode != otherMap.cachedHashCode) return false; + return rootNode.equals(otherMap.rootNode); + } + + public boolean containsValue(final Object o) { + for (Iterator iterator = valueIterator(); iterator.hasNext(); ) { + if (iterator.next().equals(o)) { + return true; + } + } + return false; + } + + public boolean isEmpty() { + return cachedSize == 0; + } + + public Iterator keyIterator() { + return new MapKeyIterator<>(rootNode); + } + + public Iterator valueIterator() { + return new MapValueIterator<>(rootNode); + } + + public Iterator> entryIterator() { + return new MapEntryIterator<>(rootNode); + } + + public Iterator> champEntryIterator() { + return new ChampEntryIterator<>(rootNode); + } + + public Set keySet() { + return new AbstractSet() { + @Override + public Iterator iterator() { + return ChampMap.this.keyIterator(); + } + + @Override + public int size() { + return ChampMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ChampMap.this.isEmpty(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public boolean contains(Object k) { + return ChampMap.this.containsKey(k); + } + }; + } + + public Collection values() { + + return new AbstractCollection() { + @Override + public Iterator iterator() { + return ChampMap.this.valueIterator(); + } + + @Override + public int size() { + return ChampMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ChampMap.this.isEmpty(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean contains(Object v) { + return ChampMap.this.containsValue(v); + } + }; + + } + + public Set> entrySet() { + return new AbstractSet>() { + @Override + public Iterator> iterator() { + return new Iterator>() { + private final Iterator> i = entryIterator(); + + @Override + public boolean hasNext() { + return i.hasNext(); + } + + @Override + public Map.Entry next() { + return i.next(); + } + + @Override + public void remove() { + i.remove(); + } + }; + } + + @Override + public int size() { + return ChampMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ChampMap.this.isEmpty(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean contains(Object k) { + throw new UnsupportedOperationException(); + } + }; + + } + + + private static abstract class BaseMapIterator { + + private static final int MAX_DEPTH = 7; + private final int[] nodeCursorsAndLengths = new int[MAX_DEPTH * 2]; + protected int currentValueCursor; + protected int currentValueLength; + protected ChampNode currentValueNode; + @SuppressWarnings("rawtypes") + ChampNode[] nodes = new ChampNode[MAX_DEPTH]; + private int currentStackLevel = -1; + + BaseMapIterator(ChampNode rootNode) { + if (rootNode.hasNodes()) { + currentStackLevel = 0; + + nodes[0] = rootNode; + nodeCursorsAndLengths[0] = 0; + nodeCursorsAndLengths[1] = rootNode.nodeArity(); + } + + if (rootNode.hasPayload()) { + currentValueNode = rootNode; + currentValueCursor = 0; + currentValueLength = rootNode.payloadArity(); + } + } + + /* + * search for next node that contains values + */ + @SuppressWarnings("unchecked") + private boolean searchNextValueNode() { + while (currentStackLevel >= 0) { + final int currentCursorIndex = currentStackLevel * 2; + final int currentLengthIndex = currentCursorIndex + 1; + + final int nodeCursor = nodeCursorsAndLengths[currentCursorIndex]; + final int nodeLength = nodeCursorsAndLengths[currentLengthIndex]; + + if (nodeCursor < nodeLength) { + final ChampNode nextNode = nodes[currentStackLevel].getNode(nodeCursor); + nodeCursorsAndLengths[currentCursorIndex]++; + + if (nextNode.hasNodes()) { + /* + * put node on next stack level for depth-first traversal + */ + final int nextStackLevel = ++currentStackLevel; + final int nextCursorIndex = nextStackLevel * 2; + final int nextLengthIndex = nextCursorIndex + 1; + + nodes[nextStackLevel] = nextNode; + nodeCursorsAndLengths[nextCursorIndex] = 0; + nodeCursorsAndLengths[nextLengthIndex] = nextNode.nodeArity(); + } + + if (nextNode.hasPayload()) { + /* + * found next node that contains values + */ + currentValueNode = nextNode; + currentValueCursor = 0; + currentValueLength = nextNode.payloadArity(); + return true; + } + } else { + currentStackLevel--; + } + } + + return false; + } + + public boolean hasNext() { + if (currentValueCursor < currentValueLength) { + return true; + } else { + return searchNextValueNode(); + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + protected static class MapKeyIterator extends BaseMapIterator + implements Iterator { + + MapKeyIterator(ChampNode rootNode) { + super(rootNode); + } + + @Override + public K next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } else { + return currentValueNode.getKey(currentValueCursor++); + } + } + + } + + protected static class MapValueIterator extends BaseMapIterator + implements Iterator { + + MapValueIterator(ChampNode rootNode) { + super(rootNode); + } + + @Override + public V next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } else { + return currentValueNode.getValue(currentValueCursor++); + } + } + + } + + protected static class MapEntryIterator extends BaseMapIterator + implements Iterator> { + + MapEntryIterator(ChampNode rootNode) { + super(rootNode); + } + + @Override + public Map.Entry next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } else { + return currentValueNode.getKeyValueEntry(currentValueCursor++); + } + } + + } + + protected static class ChampEntryIterator extends BaseMapIterator + implements Iterator> { + + ChampEntryIterator(ChampNode rootNode) { + super(rootNode); + } + + @Override + public ChampEntry next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } else { + return currentValueNode.getChampEntry(currentValueCursor++); + } + } + + } + +} diff --git a/src/main/java/com/twineworks/collections/champ/ChampNode.java b/src/main/java/com/twineworks/collections/champ/ChampNode.java new file mode 100644 index 0000000..0c72d51 --- /dev/null +++ b/src/main/java/com/twineworks/collections/champ/ChampNode.java @@ -0,0 +1,38 @@ +package com.twineworks.collections.champ; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +public interface ChampNode { + + boolean hasPayload(); + + boolean hasNodes(); + + int payloadArity(); + + int nodeArity(); + + ChampNode getNode(final int index); + + K getKey(final int index); + + V getValue(final int index); + + Map.Entry getKeyValueEntry(final int index); + + ChampEntry getChampEntry(final int index); + + boolean containsKey(final K key, final int keyHash, final int shift); + + V findByKey(final K key, final int keyHash, final int shift); + + ChampNode update(final AtomicBoolean mutable, final K key, final V val, final int keyHash, final int shift, final UpdateResult ur); + + ChampNode remove(final AtomicBoolean mutable, final K key, final int keyHash, final int shift, final UpdateResult ur); + + byte sizePredicate(); + + ChampNode dup(final AtomicBoolean mutable); + +} diff --git a/src/main/java/com/twineworks/collections/champ/CollisionNode.java b/src/main/java/com/twineworks/collections/champ/CollisionNode.java new file mode 100644 index 0000000..985605d --- /dev/null +++ b/src/main/java/com/twineworks/collections/champ/CollisionNode.java @@ -0,0 +1,254 @@ +package com.twineworks.collections.champ; + +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +final class CollisionNode implements ChampNode { + + private final K[] keys; + private final V[] vals; + private final int hash; + private final AtomicBoolean mutable; + private HashMap foo; + + CollisionNode(AtomicBoolean mutable, final int hash, final K[] keys, final V[] vals) { + this.mutable = mutable; + this.keys = keys; + this.vals = vals; + this.hash = hash; + } + + boolean isMutable() { + return mutable != null && mutable.get(); + } + + @Override + public boolean containsKey(final K key, final int keyHash, final int shift) { + if (this.hash == keyHash) { + for (K k : keys) { + if (key.equals(k)) { + return true; + } + } + } + return false; + } + + @Override + public V findByKey(final K key, final int keyHash, final int shift) { + for (int i = 0; i < keys.length; i++) { + final K _key = keys[i]; + if (key.equals(_key)) { + return vals[i]; + } + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public ChampNode update(final AtomicBoolean mutable, final K key, final V val, + final int keyHash, final int shift, final UpdateResult ur) { + + for (int idx = 0; idx < keys.length; idx++) { + if (key.equals(keys[idx])) { + final V currentVal = vals[idx]; + + if (val.equals(currentVal)) { + return this; + } else { + + if (isMutable()) { + vals[idx] = val; + return this; + } + + // replace value array + final V[] src = this.vals; + final V[] dst = (V[]) new Object[src.length]; + + // copy 'src' and set 1 element(s) at position 'idx' + System.arraycopy(src, 0, dst, 0, src.length); + dst[idx] = val; + + final ChampNode thisNew = new CollisionNode<>(mutable, this.hash, this.keys, dst); + + ur.updated(currentVal); + return thisNew; + } + } + } + + final K[] keysNew = (K[]) new Object[this.keys.length + 1]; + + // copy 'this.keys' and insert 1 element(s) at position 'keys.length' + + System.arraycopy(this.keys, 0, keysNew, 0, keys.length); + keysNew[keys.length] = key; + + final V[] valsNew = (V[]) new Object[this.vals.length + 1]; + + // copy 'this.vals' and insert 1 element(s) at position 'vals.length' + System.arraycopy(this.vals, 0, valsNew, 0, vals.length); + valsNew[vals.length] = val; + + ur.modified(); + return new CollisionNode<>(mutable, keyHash, keysNew, valsNew); + } + + @SuppressWarnings("unchecked") + @Override + public ChampNode remove(AtomicBoolean mutable, K key, int keyHash, int shift, UpdateResult ur) { + for (int idx = 0; idx < keys.length; idx++) { + if (key.equals(keys[idx])) { + final V currentVal = vals[idx]; + ur.updated(currentVal); + + if (this.arity() == 1) { + return new CompactBitmapNode<>(mutable, 0, 0, new Object[0]); + } else if (this.arity() == 2) { + + final K theOtherKey = (idx == 0) ? keys[1] : keys[0]; + final V theOtherVal = (idx == 0) ? vals[1] : vals[0]; + return new CompactBitmapNode(mutable, 0, 0, new Object[0]) + .update(mutable, theOtherKey, theOtherVal, keyHash, 0, ur); + } else { + final K[] keysNew = (K[]) new Object[this.keys.length - 1]; + + // copy 'this.keys' and remove 1 element(s) at position + // 'idx' + System.arraycopy(this.keys, 0, keysNew, 0, idx); + System.arraycopy(this.keys, idx + 1, keysNew, idx, this.keys.length - idx - 1); + + final V[] valsNew = (V[]) new Object[this.vals.length - 1]; + + // copy 'this.vals' and remove 1 element(s) at position + // 'idx' + System.arraycopy(this.vals, 0, valsNew, 0, idx); + System.arraycopy(this.vals, idx + 1, valsNew, idx, this.vals.length - idx - 1); + + return new CollisionNode<>(mutable, keyHash, keysNew, valsNew); + } + } + } + return this; + + } + + @Override + public byte sizePredicate() { + return SizePredicate.MORE_THAN_ONE; + } + + @Override + public ChampNode dup(AtomicBoolean mutable) { + return new CollisionNode<>(mutable, hash, Arrays.copyOf(keys, keys.length), Arrays.copyOf(vals, vals.length)); + } + + int arity() { + return keys.length; + } + + @Override + public boolean hasPayload() { + return true; + } + + @Override + public boolean hasNodes() { + return false; + } + + @Override + public int payloadArity() { + return keys.length; + } + + @Override + public int nodeArity() { + return 0; + } + + @Override + public ChampNode getNode(int index) { + throw new AssertionError("no nodes in collision node"); + } + + @Override + public K getKey(final int index) { + return keys[index]; + } + + @Override + public V getValue(final int index) { + return vals[index]; + } + + @Override + public Map.Entry getKeyValueEntry(int index) { + return new AbstractMap.SimpleEntry<>(keys[index], vals[index]); + } + + @Override + public ChampEntry getChampEntry(int index) { + return new ChampEntry<>(keys[index], vals[index]); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 0; + result = prime * result + hash; + result = prime * result + Arrays.hashCode(keys); + result = prime * result + Arrays.hashCode(vals); + return result; + } + + @Override + public boolean equals(final Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (getClass() != other.getClass()) { + return false; + } + + CollisionNode that = (CollisionNode) other; + + if (hash != that.hash) { + return false; + } + + if (arity() != that.arity()) { + return false; + } + + /* + * Linear scan for each key, because of arbitrary element order. + */ + outerLoop: + for (int i = 0; i < that.arity(); i++) { + final Object otherKey = that.getKey(i); + final Object otherVal = that.getValue(i); + + for (int j = 0; j < keys.length; j++) { + final K key = keys[j]; + final V val = vals[j]; + + if (key.equals(otherKey) && val.equals(otherVal)) { + continue outerLoop; + } + } + return false; + } + + return true; + } + +} diff --git a/src/main/java/com/twineworks/collections/champ/CompactBitmapNode.java b/src/main/java/com/twineworks/collections/champ/CompactBitmapNode.java new file mode 100644 index 0000000..8ff5159 --- /dev/null +++ b/src/main/java/com/twineworks/collections/champ/CompactBitmapNode.java @@ -0,0 +1,462 @@ +package com.twineworks.collections.champ; + +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +final class CompactBitmapNode implements ChampNode { + + static final int HASH_CODE_LENGTH = 32; + static final int TUPLE_LENGTH = 2; // K/V + static final int BIT_PARTITION_SIZE = 5; + static final int BIT_PARTITION_MASK = 0b11111; + + final AtomicBoolean mutable; + final int nodeMap; + final int dataMap; + final Object[] nodes; + + static int mask(final int keyHash, final int shift) { + return (keyHash >>> shift) & BIT_PARTITION_MASK; + } + + static int bitpos(final int mask) { + return 1 << mask; + } + + CompactBitmapNode(final AtomicBoolean mutable, final int nodeMap, final int dataMap, final Object[] nodes) { + this.mutable = mutable; + this.nodeMap = nodeMap; + this.dataMap = dataMap; + this.nodes = nodes; + } + + boolean isMutable() { + return mutable != null && mutable.get(); + } + + @Override + @SuppressWarnings("unchecked") + public K getKey(final int index) { + return (K) nodes[TUPLE_LENGTH * index]; + } + + @Override + @SuppressWarnings("unchecked") + public V getValue(final int index) { + return (V) nodes[TUPLE_LENGTH * index + 1]; + } + + @SuppressWarnings("unchecked") + @Override + public Map.Entry getKeyValueEntry(int index) { + return new AbstractMap.SimpleEntry( + (K) nodes[TUPLE_LENGTH * index], + (V) nodes[TUPLE_LENGTH * index + 1] + ); + } + + @SuppressWarnings("unchecked") + @Override + public ChampEntry getChampEntry(final int index) { + final int b = TUPLE_LENGTH * index; + return new ChampEntry<>( + (K) nodes[b], + (V) nodes[b + 1] + ); + } + + @Override + @SuppressWarnings("unchecked") + public ChampNode getNode(final int index) { + return (ChampNode) nodes[nodes.length - 1 - index]; + } + + int dataIndex(final int bitpos) { + return Integer.bitCount(dataMap & (bitpos - 1)); + } + + int nodeIndex(final int bitpos) { + return Integer.bitCount(nodeMap & (bitpos - 1)); + } + + ChampNode nodeAt(final int bitpos) { + return getNode(nodeIndex(bitpos)); + } + + @Override + public boolean containsKey(final K key, final int keyHash, final int shift) { + final int mask = mask(keyHash, shift); + final int bitpos = bitpos(mask); + + if ((dataMap & bitpos) != 0) { // inplace value + final int index = dataIndex(bitpos); + return key.equals(getKey(index)); + } + + if ((nodeMap & bitpos) != 0) { // node (not value) + final ChampNode subNode = nodeAt(bitpos); + return subNode.containsKey(key, keyHash, shift + BIT_PARTITION_SIZE); + } + + return false; + } + + @Override + public V findByKey(final K key, final int keyHash, final int shift) { + final int mask = mask(keyHash, shift); + final int bitpos = bitpos(mask); + + if ((dataMap & bitpos) != 0) { // inplace value + final int index = dataIndex(bitpos); + if (key.equals(getKey(index))) { + return getValue(index); + } + + return null; + } + + if ((nodeMap & bitpos) != 0) { // node (not value) + final ChampNode subNode = nodeAt(bitpos); + return subNode.findByKey(key, keyHash, shift + BIT_PARTITION_SIZE); + } + + return null; + } + + @Override + public ChampNode update(final AtomicBoolean mutable, final K key, final V val, int keyHash, int shift, final UpdateResult ur) { + + final int mask = mask(keyHash, shift); + final int bitpos = bitpos(mask); + + if ((dataMap & bitpos) != 0) { // in-place value + final int dataIndex = dataIndex(bitpos); + final K currentKey = getKey(dataIndex); + final V currentVal = getValue(dataIndex); + + if (currentKey.equals(key)) { + // refuse to update to an equal value + if (currentVal.equals(val)) { + return this; + } + // update mapping + ur.updated(currentVal); + return copyAndSetValue(mutable, bitpos, val); + } else { + final ChampNode subNodeNew = mergeTwoKeyValPairs(mutable, currentKey, currentVal, currentKey.hashCode(), key, val, keyHash, shift + BIT_PARTITION_SIZE); + ur.modified(); + return copyAndMigrateFromInlineToNode(mutable, bitpos, subNodeNew); + } + } else if ((nodeMap & bitpos) != 0) { // node (not value) + final ChampNode subNode = nodeAt(bitpos); + final ChampNode subNodeNew = subNode.update(mutable, key, val, keyHash, shift + BIT_PARTITION_SIZE, ur); + + if (ur.isModified()) { + return copyAndSetNode(mutable, bitpos, subNodeNew); + } else { + return this; + } + } else { + // no value + ur.modified(); + return copyAndInsertValue(mutable, bitpos, key, val); + } + + } + + @Override + public ChampNode remove(final AtomicBoolean mutable, final K key, final int keyHash, final int shift, final UpdateResult ur) { + + final int mask = mask(keyHash, shift); + final int bitpos = bitpos(mask); + + if ((dataMap & bitpos) != 0) { // inplace value + final int dataIndex = dataIndex(bitpos); + + if (key.equals(getKey(dataIndex))) { + final V currentVal = getValue(dataIndex); + ur.updated(currentVal); + + if (this.payloadArity() == 2 && this.nodeArity() == 0) { + final int newDataMap = (shift == 0) ? (dataMap ^ bitpos) : bitpos(mask(keyHash, 0)); + + if (dataIndex == 0) { + return new CompactBitmapNode<>(mutable, 0, newDataMap, new Object[]{getKey(1), getValue(1)}); + } else { + return new CompactBitmapNode<>(mutable, 0, newDataMap, new Object[]{getKey(0), getValue(0)}); + } + } else { + return copyAndRemoveValue(mutable, bitpos); + } + } else { + return this; + } + } else if ((nodeMap & bitpos) != 0) { // node (not value) + + final ChampNode subNode = nodeAt(bitpos); + final ChampNode subNodeNew = subNode.remove(mutable, key, keyHash, shift + BIT_PARTITION_SIZE, ur); + + if (!ur.isModified()) { + return this; + } + + switch (subNodeNew.sizePredicate()) { + case 0: { + throw new IllegalStateException("Sub-node must have at least one element."); + } + case 1: { + if (this.payloadArity() == 0 && this.nodeArity() == 1) { + // escalate (singleton or empty) result + return subNodeNew; + } else { + // inline value (move to front) + return copyAndMigrateFromNodeToInline(mutable, bitpos, subNodeNew); + } + } + default: { + // modify current node (set replacement node) + return copyAndSetNode(mutable, bitpos, subNodeNew); + } + } + } + + return this; + } + + ChampNode copyAndSetNode(final AtomicBoolean mutable, final int bitpos, final ChampNode node) { + + final int idx = this.nodes.length - 1 - nodeIndex(bitpos); + + if (isMutable()) { + // no copying if editable + this.nodes[idx] = node; + return this; + } else { + final Object[] src = this.nodes; + final Object[] dst = new Object[src.length]; + + // copy 'src' and set 1 element(s) at position 'idx' + System.arraycopy(src, 0, dst, 0, src.length); + dst[idx] = node; + + return new CompactBitmapNode<>(mutable, nodeMap, dataMap, dst); + } + } + + ChampNode copyAndSetValue(final AtomicBoolean mutable, final int bitpos, final V val) { + + final int idx = TUPLE_LENGTH * dataIndex(bitpos) + 1; + + if (isMutable()) { + // no copying if editable + this.nodes[idx] = val; + return this; + } else { + final Object[] src = this.nodes; + final Object[] dst = new Object[src.length]; + + // copy 'src' and set 1 element(s) at position 'idx' + System.arraycopy(src, 0, dst, 0, src.length); + dst[idx] = val; + + return new CompactBitmapNode<>(mutable, nodeMap, dataMap, dst); + } + } + + ChampNode copyAndInsertValue(final AtomicBoolean mutable, final int bitpos, final K key, final V val) { + + final int idx = TUPLE_LENGTH * dataIndex(bitpos); + + final Object[] src = this.nodes; + final Object[] dst = new Object[src.length + 2]; + + // copy 'src' and insert 2 element(s) at position 'idx' + System.arraycopy(src, 0, dst, 0, idx); + dst[idx] = key; + dst[idx + 1] = val; + System.arraycopy(src, idx, dst, idx + 2, src.length - idx); + + return new CompactBitmapNode(mutable, nodeMap, dataMap | bitpos, dst); + } + + @SuppressWarnings("unchecked") + ChampNode mergeTwoKeyValPairs(final AtomicBoolean mutable, final K key0, final V val0, final int keyHash0, final K key1, final V val1, final int keyHash1, final int shift) { + + if (shift >= HASH_CODE_LENGTH) { + return new CollisionNode<>(mutable, keyHash0, (K[]) new Object[]{key0, key1}, (V[]) new Object[]{val0, val1}); + } + + final int mask0 = mask(keyHash0, shift); + final int mask1 = mask(keyHash1, shift); + + if (mask0 != mask1) { + // both nodes fit on same level + final int dataMap = bitpos(mask0) | bitpos(mask1); + + if (mask0 < mask1) { + return new CompactBitmapNode<>(mutable, (0), dataMap, new Object[]{key0, val0, key1, val1}); + } else { + return new CompactBitmapNode<>(mutable, (0), dataMap, new Object[]{key1, val1, key0, val0}); + } + } else { + final ChampNode node = mergeTwoKeyValPairs(mutable, key0, val0, keyHash0, key1, val1, keyHash1, shift + BIT_PARTITION_SIZE); + // values fit on next level + final int nodeMap = bitpos(mask0); + return new CompactBitmapNode<>(mutable, nodeMap, (0), new Object[]{node}); + } + } + + ChampNode copyAndMigrateFromInlineToNode(final AtomicBoolean mutable, final int bitpos, final ChampNode node) { + + final int idxOld = TUPLE_LENGTH * dataIndex(bitpos); + final int idxNew = this.nodes.length - TUPLE_LENGTH - nodeIndex(bitpos); + + final Object[] src = this.nodes; + final Object[] dst = new Object[src.length - 2 + 1]; + + // copy 'src' and remove 2 element(s) at position 'idxOld' and + // insert 1 element(s) at position 'idxNew' + + System.arraycopy(src, 0, dst, 0, idxOld); + System.arraycopy(src, idxOld + 2, dst, idxOld, idxNew - idxOld); + dst[idxNew] = node; + System.arraycopy(src, idxNew + 2, dst, idxNew + 1, src.length - idxNew - 2); + + return new CompactBitmapNode<>(mutable, nodeMap | bitpos, dataMap ^ bitpos, dst); + } + + ChampNode copyAndMigrateFromNodeToInline(final AtomicBoolean mutable, final int bitpos, final ChampNode node) { + + final int idxOld = this.nodes.length - 1 - nodeIndex(bitpos); + final int idxNew = TUPLE_LENGTH * dataIndex(bitpos); + + final Object[] src = this.nodes; + final Object[] dst = new Object[src.length - 1 + 2]; + + // copy 'src' and remove 1 element(s) at position 'idxOld' and + // insert 2 element(s) at position 'idxNew' + assert idxOld >= idxNew; + System.arraycopy(src, 0, dst, 0, idxNew); + dst[idxNew] = node.getKey(0); + dst[idxNew + 1] = node.getValue(0); + System.arraycopy(src, idxNew, dst, idxNew + 2, idxOld - idxNew); + System.arraycopy(src, idxOld + 1, dst, idxOld + 2, src.length - idxOld - 1); + + return new CompactBitmapNode<>(mutable, nodeMap ^ bitpos, dataMap | bitpos, dst); + } + + ChampNode copyAndRemoveValue(final AtomicBoolean mutable, final int bitpos) { + final int idx = TUPLE_LENGTH * dataIndex(bitpos); + + final Object[] src = this.nodes; + final Object[] dst = new Object[src.length - 2]; + + // copy 'src' and remove 2 element(s) at position 'idx' + System.arraycopy(src, 0, dst, 0, idx); + System.arraycopy(src, idx + 2, dst, idx, src.length - idx - 2); + + return new CompactBitmapNode<>(mutable, nodeMap, dataMap ^ bitpos, dst); + } + + @Override + public int payloadArity() { + return Integer.bitCount(dataMap); + } + + @Override + public int nodeArity() { + return Integer.bitCount(nodeMap); + } + + int slotArity() { + return nodes.length; + } + + @Override + public byte sizePredicate() { + if (nodeArity() == 0) { + switch (payloadArity()) { + case 0: + return SizePredicate.EMPTY; + case 1: + return SizePredicate.ONE; + default: + return SizePredicate.MORE_THAN_ONE; + } + } else { + return SizePredicate.MORE_THAN_ONE; + } + } + + @SuppressWarnings("unchecked") + @Override + public ChampNode dup(AtomicBoolean mutable) { + + // useful only on transient nodes, to create template transients with pre-allocated memory structures + // which can be copied once, then their values are set. + + final Object[] src = this.nodes; + final Object[] dst = new Object[src.length]; + + int firstNodeAt = TUPLE_LENGTH * payloadArity(); + + // copy all values + System.arraycopy(src, 0, dst, 0, firstNodeAt); + + // dup all nodes + for (int i = firstNodeAt; i < dst.length; i++) { + dst[i] = ((ChampNode) src[i]).dup(mutable); + } + + return new CompactBitmapNode<>(mutable, nodeMap, dataMap, dst); + + } + + @Override + public boolean equals(final Object other) { + + if (null == other) { + return false; + } + + if (this == other) { + return true; + } + + if (getClass() != other.getClass()) { + return false; + } + CompactBitmapNode that = (CompactBitmapNode) other; + if (nodeMap != that.nodeMap) { + return false; + } + if (dataMap != that.dataMap) { + return false; + } + + return Arrays.equals(this.nodes, that.nodes); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 0; + result = prime * result + nodeMap; + result = prime * result + dataMap; + result = prime * result + Arrays.hashCode(nodes); + return result; + } + + @Override + public boolean hasPayload() { + return dataMap != 0; + } + + @Override + public boolean hasNodes() { + return nodeMap != 0; + } + +} diff --git a/src/main/java/com/twineworks/collections/champ/SizePredicate.java b/src/main/java/com/twineworks/collections/champ/SizePredicate.java new file mode 100644 index 0000000..ae121c8 --- /dev/null +++ b/src/main/java/com/twineworks/collections/champ/SizePredicate.java @@ -0,0 +1,7 @@ +package com.twineworks.collections.champ; + +final public class SizePredicate { + final static byte EMPTY = 0b00; + final static byte ONE = 0b01; + final static byte MORE_THAN_ONE = 0b10; +} diff --git a/src/main/java/com/twineworks/collections/champ/TransientChampMap.java b/src/main/java/com/twineworks/collections/champ/TransientChampMap.java new file mode 100644 index 0000000..441dbbd --- /dev/null +++ b/src/main/java/com/twineworks/collections/champ/TransientChampMap.java @@ -0,0 +1,219 @@ +package com.twineworks.collections.champ; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +public class TransientChampMap { + + final private AtomicBoolean mutable; + private ChampNode rootNode; + private int cachedHashCode; + private int cachedSize; + + public TransientChampMap() { + this.mutable = new AtomicBoolean(true); + ChampMap src = ChampMap.empty(); + this.rootNode = src.rootNode; + this.cachedHashCode = src.cachedHashCode; + this.cachedSize = src.cachedSize; + } + + public TransientChampMap(TransientChampMap src) { + this.mutable = new AtomicBoolean(true); + this.rootNode = src.rootNode.dup(this.mutable); + this.cachedHashCode = src.cachedHashCode; + this.cachedSize = src.cachedSize; + } + + public TransientChampMap(ChampMap src) { + this.mutable = new AtomicBoolean(true); + this.rootNode = src.rootNode; + this.cachedHashCode = src.cachedHashCode; + this.cachedSize = src.cachedSize; + } + + public TransientChampMap dup() { + return new TransientChampMap<>(this); + } + + public V get(K key) { + return rootNode.findByKey(key, key.hashCode(), 0); + } + + public boolean containsKey(K key) { + return rootNode.containsKey(key, key.hashCode(), 0); + } + + public int size() { + return cachedSize; + } + + public void set(final K key, final V val) { + if (!mutable.get()) { + throw new IllegalStateException("Transient already frozen."); + } + + final int keyHash = key.hashCode(); + final UpdateResult ur = UpdateResult.unchanged(); + + final ChampNode newRootNode = rootNode.update(mutable, key, val, keyHash, 0, ur); + + if (ur.isModified()) { + if (ur.hasReplacedValue()) { + final V old = ur.getReplacedValue(); + + final int valHashOld = old.hashCode(); + final int valHashNew = val.hashCode(); + + rootNode = newRootNode; + cachedHashCode = cachedHashCode + (keyHash ^ valHashNew) - (keyHash ^ valHashOld); + } else { + final int valHashNew = val.hashCode(); + rootNode = newRootNode; + cachedHashCode += (keyHash ^ valHashNew); + cachedSize += 1; + } + } + + } + + public void setAll(final Iterator> iter) { + + if (!mutable.get()) { + throw new IllegalStateException("Transient already frozen."); + } + + final UpdateResult ur = UpdateResult.unchanged(); + + while (iter.hasNext()) { + final Map.Entry entry = iter.next(); + + K key = entry.getKey(); + V val = entry.getValue(); + + final int keyHash = key.hashCode(); + + final ChampNode newRootNode = rootNode.update(mutable, key, val, keyHash, 0, ur); + + if (ur.isModified()) { + if (ur.hasReplacedValue()) { + final V old = ur.getReplacedValue(); + + final int valHashOld = old.hashCode(); + final int valHashNew = val.hashCode(); + + rootNode = newRootNode; + cachedHashCode = cachedHashCode + (keyHash ^ valHashNew) - (keyHash ^ valHashOld); + } else { + final int valHashNew = val.hashCode(); + rootNode = newRootNode; + cachedHashCode += (keyHash ^ valHashNew); + cachedSize += 1; + } + ur.reset(); + } + + } + + } + + public void setAll(Iterable> entries) { + setAll(entries.iterator()); + } + + public void setAll(Map src) { + setAll(src.entrySet()); + } + + public void setAll(ChampMap src) { + + if (!mutable.get()) { + throw new IllegalStateException("Transient already frozen."); + } + + final UpdateResult ur = UpdateResult.unchanged(); + + final Iterator> iter = src.champEntryIterator(); + while (iter.hasNext()) { + + final ChampEntry entry = iter.next(); + + K key = entry.key; + V val = entry.value; + + final int keyHash = key.hashCode(); + + final ChampNode newRootNode = rootNode.update(mutable, key, val, keyHash, 0, ur); + + if (ur.isModified()) { + if (ur.hasReplacedValue()) { + final V old = ur.getReplacedValue(); + + final int valHashOld = old.hashCode(); + final int valHashNew = val.hashCode(); + + rootNode = newRootNode; + cachedHashCode = cachedHashCode + (keyHash ^ valHashNew) - (keyHash ^ valHashOld); + } else { + final int valHashNew = val.hashCode(); + rootNode = newRootNode; + cachedHashCode += (keyHash ^ valHashNew); + cachedSize += 1; + } + ur.reset(); + } + + } + + } + + public void remove(K key) { + + final int keyHash = key.hashCode(); + final UpdateResult ur = UpdateResult.unchanged(); + + final ChampNode newRootNode = rootNode.remove(mutable, key, keyHash, 0, ur); + + if (ur.isModified()) { + final int valHash = ur.getReplacedValue().hashCode(); + rootNode = newRootNode; + cachedHashCode -= (keyHash ^ valHash); + cachedSize--; + } + + } + + public void removeAll(Collection keys){ + + final UpdateResult ur = UpdateResult.unchanged(); + + for (K key : keys) { + final int keyHash = key.hashCode(); + + final ChampNode newRootNode = rootNode.remove(mutable, key, keyHash, 0, ur); + + if (ur.isModified()) { + final int valHash = ur.getReplacedValue().hashCode(); + rootNode = newRootNode; + cachedHashCode -= (keyHash ^ valHash); + cachedSize--; + ur.reset(); + } + + } + + } + + public ChampMap freeze() { + if (!mutable.get()) { + throw new IllegalStateException("Transient already frozen."); + } + + mutable.set(false); + return new ChampMap(rootNode, cachedHashCode, cachedSize); + + } + +} diff --git a/src/main/java/com/twineworks/collections/champ/UpdateResult.java b/src/main/java/com/twineworks/collections/champ/UpdateResult.java new file mode 100644 index 0000000..204de16 --- /dev/null +++ b/src/main/java/com/twineworks/collections/champ/UpdateResult.java @@ -0,0 +1,45 @@ +package com.twineworks.collections.champ; + +class UpdateResult { + + private V replacedValue; + private boolean isModified; + private boolean isReplaced; + + private UpdateResult() { + } + + public static UpdateResult unchanged() { + return new UpdateResult<>(); + } + + public void modified() { + this.isModified = true; + } + + public void updated(V replacedValue) { + this.replacedValue = replacedValue; + this.isModified = true; + this.isReplaced = true; + } + + public void reset() { + replacedValue = null; + isModified = false; + isReplaced = false; + } + + public boolean isModified() { + return isModified; + } + + public boolean hasReplacedValue() { + return isReplaced; + } + + public V getReplacedValue() { + return replacedValue; + } + +} + diff --git a/src/test/java/com/twineworks/collections/champ/ChampMapTest.java b/src/test/java/com/twineworks/collections/champ/ChampMapTest.java new file mode 100644 index 0000000..57302ba --- /dev/null +++ b/src/test/java/com/twineworks/collections/champ/ChampMapTest.java @@ -0,0 +1,276 @@ +package com.twineworks.collections.champ; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ChampMapTest { + + private static class Collider{ + private final String name; + + private Collider(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Collider collider = (Collider) o; + return Objects.equals(name, collider.name); + } + + @Override + public int hashCode() { + // always collide + return 42; + } + } + + private static ArrayList strKeys = new ArrayList<>(); + + static { + for (char c = 10; c < 128; c++){ + for (char d = 10; d < 128; d++){ + strKeys.add(String.valueOf(c) + d); + } + } + } + + @Test + public void empty_has_zero_size() { + assertThat(ChampMap.empty().size()).isEqualTo(0); + } + + @Test + public void insert_into_empty() { + + ChampMap map = ChampMap.empty(); + + map = map.set("a",1L); + assertThat(map.size()).isEqualTo(1); + + assertThat(map.containsKey("a")).isTrue(); + assertThat(map.get("a")).isEqualTo(1L); + + assertThat(map.containsKey("b")).isFalse(); + assertThat(map.get("b")).isNull(); + } + + @Test + public void insert_and_remove_many() { + + ChampMap map = ChampMap.empty(); + + for(String k: strKeys){ + map = map.set(k, k); + assertThat(map.containsKey(k)).isTrue(); + assertThat(map.get(k)).isEqualTo(k); + } + + assertThat(map.size()).isEqualTo(strKeys.size()); + + int size = map.size(); + for (String s : map.keySet()) { + + map = map.remove(s); + size--; + + assertThat(map.containsKey(s)).isFalse(); + assertThat(map.get(s)).isNull(); + assertThat(map.size()).isEqualTo(size); + } + + } + + @Test + public void insert_into_and_remove_from_empty() { + + ChampMap map = ChampMap.empty(); + + map = map.set("a",1L); + assertThat(map.size()).isEqualTo(1); + map = map.remove("a"); + + assertThat(map.containsKey("a")).isFalse(); + assertThat(map.get("a")).isNull(); + } + + @Test + public void reset_in_singleton() { + + ChampMap map = ChampMap.empty(); + + map = map.set("a",1L); + map = map.set("a",1L); + assertThat(map.size()).isEqualTo(1); + + assertThat(map.containsKey("a")).isTrue(); + assertThat(map.get("a")).isEqualTo(1L); + + assertThat(map.containsKey("b")).isFalse(); + assertThat(map.get("b")).isNull(); + } + + @Test + public void replace_in_singleton() { + + ChampMap map = ChampMap.empty(); + + map = map.set("a",1L); + map = map.set("a",2L); + assertThat(map.size()).isEqualTo(1); + + assertThat(map.containsKey("a")).isTrue(); + assertThat(map.get("a")).isEqualTo(2L); + + assertThat(map.containsKey("b")).isFalse(); + assertThat(map.get("b")).isNull(); + } + + @Test + public void split_common_prefix_insert() { + + ChampMap map = ChampMap.empty(); + + map = map.set(1L, 1L); + map = map.set(1025L, 2L); + assertThat(map.size()).isEqualTo(2); + + assertThat(map.containsKey(1L)).isTrue(); + assertThat(map.get(1L)).isEqualTo(1L); + + assertThat(map.containsKey(1025L)).isTrue(); + assertThat(map.get(1025L)).isEqualTo(2L); + + assertThat(map.containsKey(2L)).isFalse(); + assertThat(map.get(2L)).isNull(); + } + + + @Test + public void hash_collision_insert() { + + ChampMap map = ChampMap.empty(); + + Collider a = new Collider("a"); + Collider b = new Collider("b"); + Collider c = new Collider("c"); + + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + + map = map.set(a, 1L); + map = map.set(b, 2L); + assertThat(map.size()).isEqualTo(2); + + assertThat(map.containsKey(a)).isTrue(); + assertThat(map.get(a)).isEqualTo(1L); + + assertThat(map.containsKey(b)).isTrue(); + assertThat(map.get(b)).isEqualTo(2L); + + assertThat(map.containsKey(c)).isFalse(); + assertThat(map.get(c)).isNull(); + } + + @Test + public void iterates_over_keys() { + + ChampMap map = ChampMap.empty(); + + map = map.set("a",1L); + map = map.set("b",2L); + map = map.set("c",3L); + assertThat(map.size()).isEqualTo(3); + + ArrayList keys = new ArrayList<>(); + Iterator ki = map.keyIterator(); + while(ki.hasNext()){ + keys.add(ki.next()); + } + + assertThat(keys).containsExactly("a", "b", "c"); + + } + + @Test + public void key_set() { + + ChampMap map = ChampMap.empty(); + + map = map.set("a",1L); + map = map.set("b",2L); + map = map.set("c",3L); + assertThat(map.size()).isEqualTo(3); + + assertThat(map.keySet()).containsExactly("a", "b", "c"); + + } + + @Test + public void values() { + + ChampMap map = ChampMap.empty(); + + map = map.set("a",1L); + map = map.set("b",2L); + map = map.set("c",3L); + assertThat(map.size()).isEqualTo(3); + + assertThat(map.values()).containsExactly(1L, 2L, 3L); + + } + + @Test + public void iterates_over_values() { + + ChampMap map = ChampMap.empty(); + + map = map.set("a",1L); + map = map.set("b",2L); + map = map.set("c",3L); + assertThat(map.size()).isEqualTo(3); + + ArrayList values = new ArrayList<>(); + Iterator vi = map.valueIterator(); + while(vi.hasNext()){ + values.add(vi.next()); + } + + assertThat(values).containsExactly(1L, 2L, 3L); + + } + + @Test + public void iterates_over_entries() { + + ChampMap map = ChampMap.empty(); + + map = map.set("a",1L); + map = map.set("b",2L); + map = map.set("c",3L); + assertThat(map.size()).isEqualTo(3); + + ArrayList keys = new ArrayList<>(); + ArrayList values = new ArrayList<>(); + Iterator> vi = map.entryIterator(); + while(vi.hasNext()){ + Map.Entry n = vi.next(); + keys.add(n.getKey()); + values.add(n.getValue()); + // a=1, b=2 etc. + assertThat(n.getKey().charAt(0)).isEqualTo((char)('a'+n.getValue()-1)); + } + + assertThat(keys).containsExactly("a", "b", "c"); + assertThat(values).containsExactly(1L, 2L, 3L); + + } + +} diff --git a/src/test/java/com/twineworks/collections/champ/TransientChampMapTest.java b/src/test/java/com/twineworks/collections/champ/TransientChampMapTest.java new file mode 100644 index 0000000..7694abe --- /dev/null +++ b/src/test/java/com/twineworks/collections/champ/TransientChampMapTest.java @@ -0,0 +1,280 @@ +package com.twineworks.collections.champ; + +import org.junit.Test; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TransientChampMapTest { + + private static class Collider{ + private final String name; + + private Collider(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Collider collider = (Collider) o; + return Objects.equals(name, collider.name); + } + + @Override + public int hashCode() { + // always collide + return 42; + } + } + + private static ArrayList strKeys = new ArrayList<>(); + + static { + for (char c = 1; c < 128; c++){ + for (char d = 1; d < 128; d++){ + for (char e = 1; d < 128; d++) { + strKeys.add((String.valueOf(c) + d) + e); + } + } + } + } + + @Test + public void empty_has_zero_size() { + assertThat(new TransientChampMap<>().size()).isEqualTo(0); + } + + @Test + public void insert_into_empty() { + + TransientChampMap map = new TransientChampMap<>(); + + map.set("a",1L); + assertThat(map.size()).isEqualTo(1); + + assertThat(map.containsKey("a")).isTrue(); + assertThat(map.get("a")).isEqualTo(1L); + + assertThat(map.containsKey("b")).isFalse(); + assertThat(map.get("b")).isNull(); + } + + @Test + public void reset_in_singleton() { + + TransientChampMap map = new TransientChampMap<>(); + + map.set("a",1L); + map.set("a",1L); + assertThat(map.size()).isEqualTo(1); + + assertThat(map.containsKey("a")).isTrue(); + assertThat(map.get("a")).isEqualTo(1L); + + assertThat(map.containsKey("b")).isFalse(); + assertThat(map.get("b")).isNull(); + } + + @Test + public void replace_in_singleton() { + + TransientChampMap map = new TransientChampMap<>(); + + map.set("a",1L); + map.set("a",2L); + assertThat(map.size()).isEqualTo(1); + + assertThat(map.containsKey("a")).isTrue(); + assertThat(map.get("a")).isEqualTo(2L); + + assertThat(map.containsKey("b")).isFalse(); + assertThat(map.get("b")).isNull(); + } + + @Test + public void split_common_prefix_insert() { + + TransientChampMap map = new TransientChampMap<>(); + + map.set(1L, 1L); + map.set(1025L, 2L); + assertThat(map.size()).isEqualTo(2); + + assertThat(map.containsKey(1L)).isTrue(); + assertThat(map.get(1L)).isEqualTo(1L); + + assertThat(map.containsKey(1025L)).isTrue(); + assertThat(map.get(1025L)).isEqualTo(2L); + + assertThat(map.containsKey(2L)).isFalse(); + assertThat(map.get(2L)).isNull(); + } + + + @Test + public void hash_collision_insert() { + + TransientChampMap map = new TransientChampMap<>(); + + Collider a = new Collider("a"); + Collider b = new Collider("b"); + Collider c = new Collider("c"); + + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + + map.set(a, 1L); + map.set(b, 2L); + assertThat(map.size()).isEqualTo(2); + + assertThat(map.containsKey(a)).isTrue(); + assertThat(map.get(a)).isEqualTo(1L); + + assertThat(map.containsKey(b)).isTrue(); + assertThat(map.get(b)).isEqualTo(2L); + + assertThat(map.containsKey(c)).isFalse(); + assertThat(map.get(c)).isNull(); + } + + @Test + public void insert_and_remove_many() { + + TransientChampMap map = new TransientChampMap<>(); + + for(String k: strKeys){ + map.set(k, k); + assertThat(map.containsKey(k)).isTrue(); + assertThat(map.get(k)).isEqualTo(k); + } + + assertThat(map.size()).isEqualTo(strKeys.size()); + + int size = map.size(); + for (String s : strKeys) { + + map.remove(s); + size--; + + assertThat(map.containsKey(s)).isFalse(); + assertThat(map.get(s)).isNull(); + assertThat(map.size()).isEqualTo(size); + } + + } + + @Test + public void insert_into_and_remove_from_empty() { + + TransientChampMap map = new TransientChampMap<>(); + + map.set("a",1L); + assertThat(map.size()).isEqualTo(1); + map.remove("a"); + + assertThat(map.size()).isEqualTo(0); + assertThat(map.containsKey("a")).isFalse(); + assertThat(map.get("a")).isNull(); + } + + @Test + public void freezes_after_inserts() { + + TransientChampMap map = new TransientChampMap<>(); + + map.set(0L, 0L); + map.set(1L, 1L); + map.set(2L, 2L); + map.set(3L, 3L); + map.set(64L, 64L); + map.set(1025L, 1025L); + + ChampMap p = map.freeze(); + Iterator> iter = p.entryIterator(); + + ArrayList ks = new ArrayList<>(); + ArrayList vs = new ArrayList<>(); + + while(iter.hasNext()){ + Map.Entry entry = iter.next(); + ks.add(entry.getKey()); + vs.add(entry.getValue()); + } + + assertThat(ks).isEqualTo(vs); + + ks.sort(Comparator.comparingLong((x) -> x)); + + assertThat(ks.get(0)).isEqualTo(0L); + assertThat(ks.get(1)).isEqualTo(1L); + assertThat(ks.get(2)).isEqualTo(2L); + assertThat(ks.get(3)).isEqualTo(3L); + assertThat(ks.get(4)).isEqualTo(64L); + assertThat(ks.get(5)).isEqualTo(1025L); + + } + + @Test + public void compares_as_equal_after_inserts() { + + TransientChampMap map = new TransientChampMap<>(); + map.set(0L, 0L); + map.set(1L, 1L); + map.set(2L, 2L); + map.set(3L, 3L); + map.set(64L, 64L); + map.set(1025L, 1025L); + ChampMap p = map.freeze(); + + ChampMap p2 = ChampMap.empty(); + p2 = p2.set(0L, 0L); + p2 = p2.set(1L, 1L); + p2 = p2.set(2L, 2L); + p2 = p2.set(3L, 3L); + p2 = p2.set(64L, 64L); + p2 = p2.set(1025L, 1025L); + + assertThat(p).isEqualTo(p2); + + } + + @Test + public void compares_as_equal_after_insert_and_remove_many() { + + TransientChampMap map = new TransientChampMap<>(); + ChampMap p2 = ChampMap.empty(); + + for(String k: strKeys){ + map.set(k, k); + p2 = p2.set(k, k); + } + + for (String s : strKeys) { + map.remove(s); + p2 = p2.remove(s); + } + ChampMap p1 = map.freeze(); + + assertThat(p1).isEqualTo(p2); + + } + + @Test + public void compares_as_equal_after_insert_many() { + + TransientChampMap map = new TransientChampMap<>(); + ChampMap p2 = ChampMap.empty(); + + for(String k: strKeys){ + map.set(k, k); + p2 = p2.set(k, k); + } + + ChampMap p1 = map.freeze(); + assertThat(p1).isEqualTo(p2); + + } + +}