diff --git a/agrona/src/main/java/org/agrona/collections/Int2NullableObjectHashMap.java b/agrona/src/main/java/org/agrona/collections/Int2NullableObjectHashMap.java new file mode 100644 index 000000000..826884c05 --- /dev/null +++ b/agrona/src/main/java/org/agrona/collections/Int2NullableObjectHashMap.java @@ -0,0 +1,73 @@ +/* + * Copyright 2014-2018 Real Logic Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.agrona.collections; + +import org.agrona.generation.DoNotSub; + +/** + * Variation of {@link Int2ObjectHashMap} that allows {@code null} values. + * + * @param type of values stored in the {@link java.util.Map} + */ +public class Int2NullableObjectHashMap extends Int2ObjectHashMap +{ + public Int2NullableObjectHashMap() + { + } + + public Int2NullableObjectHashMap( + @DoNotSub final int initialCapacity, + final float loadFactor) + { + super(initialCapacity, loadFactor); + } + + /** + * Construct a new map allowing a configuration for initial capacity and load factor. + * @param initialCapacity for the backing array + * @param loadFactor limit for resizing on puts + * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. + */ + public Int2NullableObjectHashMap( + @DoNotSub final int initialCapacity, + final float loadFactor, + final boolean shouldAvoidAllocation) + { + super(initialCapacity, loadFactor, shouldAvoidAllocation); + } + + /** + * Copy construct a new map from an existing one. + * + * @param mapToCopy for construction. + */ + public Int2NullableObjectHashMap(final Int2ObjectHashMap mapToCopy) + { + super(mapToCopy); + } + + @Override + protected Object mapNullValue(final Object value) + { + return value == null ? NullReference.INSTANCE : value; + } + + @Override + protected V unmapNullValue(final Object value) + { + return value == NullReference.INSTANCE ? null : (V)value; + } +} diff --git a/agrona/src/main/java/org/agrona/collections/Int2ObjectHashMap.java b/agrona/src/main/java/org/agrona/collections/Int2ObjectHashMap.java index fde833f18..2461c67a3 100644 --- a/agrona/src/main/java/org/agrona/collections/Int2ObjectHashMap.java +++ b/agrona/src/main/java/org/agrona/collections/Int2ObjectHashMap.java @@ -186,11 +186,12 @@ public boolean containsKey(final int key) public boolean containsValue(final Object value) { boolean found = false; - if (null != value) + final Object val = mapNullValue(value); + if (null != val) { for (final Object v : values) { - if (value.equals(v)) + if (val.equals(v)) { found = true; break; @@ -215,8 +216,13 @@ public V get(final Object key) * @param key for indexing the {@link Map} * @return the value if found otherwise null */ - @SuppressWarnings("unchecked") public V get(final int key) + { + return unmapNullValue(getMapped(key)); + } + + @SuppressWarnings("unchecked") + protected V getMapped(final int key) { @DoNotSub final int mask = values.length - 1; @DoNotSub int index = Hashing.hash(key, mask); @@ -247,7 +253,7 @@ public V get(final int key) */ public V computeIfAbsent(final int key, final IntFunction mappingFunction) { - V value = get(key); + V value = getMapped(key); if (value == null) { value = mappingFunction.apply(key); @@ -256,6 +262,10 @@ public V computeIfAbsent(final int key, final IntFunction mappingFu put(key, value); } } + else + { + value = unmapNullValue(value); + } return value; } @@ -278,7 +288,8 @@ public V put(final Integer key, final V value) @SuppressWarnings("unchecked") public V put(final int key, final V value) { - requireNonNull(value, "Value cannot be null"); + final V val = (V)mapNullValue(value); + requireNonNull(val, "Value cannot be null"); V oldValue = null; @DoNotSub final int mask = values.length - 1; @@ -301,14 +312,14 @@ public V put(final int key, final V value) keys[index] = key; } - values[index] = value; + values[index] = val; if (size > resizeThreshold) { increaseCapacity(); } - return oldValue; + return unmapNullValue(oldValue); } /** @@ -346,7 +357,7 @@ public V remove(final int key) index = ++index & mask; } - return (V)value; + return unmapNullValue(value); } /** @@ -438,7 +449,7 @@ public String toString() while (true) { entryIterator.next(); - sb.append(entryIterator.getIntKey()).append('=').append(entryIterator.getValue()); + sb.append(entryIterator.getIntKey()).append('=').append(unmapNullValue(entryIterator.getValue())); if (!entryIterator.hasNext()) { return sb.append('}').toString(); @@ -475,7 +486,7 @@ public boolean equals(final Object o) if (null != thisValue) { final Object thatValue = that.get(keys[i]); - if (!thisValue.equals(thatValue)) + if (!thisValue.equals(mapNullValue(thatValue))) { return false; } @@ -504,6 +515,16 @@ public boolean equals(final Object o) return result; } + protected Object mapNullValue(final Object value) + { + return value; + } + + protected V unmapNullValue(final Object value) + { + return (V)value; + } + /** * Primitive specialised version of {@link #replace(Object, Object)} * @@ -534,7 +555,7 @@ public V replace(final int key, final V value) public boolean replace(final int key, final V oldValue, final V newValue) { final Object curValue = get(key); - if (curValue == null || !Objects.equals(curValue, oldValue)) + if (curValue == null || !Objects.equals(unmapNullValue(curValue), oldValue)) { return false; } @@ -735,8 +756,9 @@ public void clear() public boolean contains(final Object o) { final Entry entry = (Entry)o; - final V value = get(entry.getKey()); - return value != null && value.equals(entry.getValue()); + final int key = ((Integer)entry.getKey()).intValue(); + final V value = getMapped(key); + return value != null && value.equals(mapNullValue(entry.getValue())); } } @@ -843,7 +865,7 @@ public V next() { findNext(); - return (V)values[position()]; + return unmapNullValue(values[position()]); } } @@ -902,7 +924,7 @@ public V setValue(final V value) @DoNotSub public int hashCode() { - return Integer.hashCode(getIntKey()) ^ v.hashCode(); + return Integer.hashCode(getIntKey()) ^ (v != null ? v.hashCode() : 0); } @DoNotSub public boolean equals(final Object o) @@ -914,8 +936,8 @@ public V setValue(final V value) final Map.Entry e = (Entry)o; - return (e.getKey() != null && e.getValue() != null) && - (e.getKey().equals(k) && e.getValue().equals(v)); + return (e.getKey() != null && e.getKey().equals(k)) && + ((e.getValue() == null && v == null) || e.getValue().equals(v)); } public String toString() @@ -937,12 +959,13 @@ public int getIntKey() public V getValue() { - return (V)values[position()]; + return unmapNullValue(values[position()]); } public V setValue(final V value) { - requireNonNull(value); + final V val = (V)mapNullValue(value); + requireNonNull(val, "Value cannot be null"); if (!this.isPositionValid) { @@ -951,7 +974,7 @@ public V setValue(final V value) @DoNotSub final int pos = position(); final Object oldValue = values[pos]; - values[pos] = value; + values[pos] = val; return (V)oldValue; } diff --git a/agrona/src/main/java/org/agrona/collections/NullReference.java b/agrona/src/main/java/org/agrona/collections/NullReference.java new file mode 100644 index 000000000..9a6bb92f6 --- /dev/null +++ b/agrona/src/main/java/org/agrona/collections/NullReference.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014-2018 Real Logic Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.agrona.collections; + +public final class NullReference +{ + public static final NullReference INSTANCE = new NullReference(); + + public int hashCode() + { + return 0; + } + + public boolean equals(final Object obj) + { + return obj == this; + } +} diff --git a/agrona/src/main/java/org/agrona/collections/Object2NullableObjectHashMap.java b/agrona/src/main/java/org/agrona/collections/Object2NullableObjectHashMap.java new file mode 100644 index 000000000..f6cc98208 --- /dev/null +++ b/agrona/src/main/java/org/agrona/collections/Object2NullableObjectHashMap.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2018 Real Logic Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.agrona.collections; + +/** + * Variation of {@link Object2ObjectHashMap} that allows {@code null} values. + */ +public class Object2NullableObjectHashMap extends Object2ObjectHashMap +{ + public Object2NullableObjectHashMap() + { + } + + public Object2NullableObjectHashMap( + final int initialCapacity, + final float loadFactor) + { + super(initialCapacity, loadFactor); + } + + /** + * @param initialCapacity for the map to override {@link #MIN_CAPACITY} + * @param loadFactor for the map to override {@link Hashing#DEFAULT_LOAD_FACTOR}. + * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. + */ + public Object2NullableObjectHashMap( + final int initialCapacity, + final float loadFactor, + final boolean shouldAvoidAllocation) + { + super(initialCapacity, loadFactor, shouldAvoidAllocation); + } + + @Override + protected Object mapNullValue(final Object value) + { + return value == null ? NullReference.INSTANCE : value; + } + + @Override + protected V unmapNullValue(final Object value) + { + return value == NullReference.INSTANCE ? null : (V)value; + } +} diff --git a/agrona/src/main/java/org/agrona/collections/Object2ObjectHashMap.java b/agrona/src/main/java/org/agrona/collections/Object2ObjectHashMap.java index ec45e7115..3049d5f29 100644 --- a/agrona/src/main/java/org/agrona/collections/Object2ObjectHashMap.java +++ b/agrona/src/main/java/org/agrona/collections/Object2ObjectHashMap.java @@ -118,8 +118,13 @@ public boolean isEmpty() return size == 0; } - @SuppressWarnings("unchecked") public V get(final Object key) + { + return unmapNullValue(getMapped(key)); + } + + @SuppressWarnings("unchecked") + private V getMapped(final Object key) { Objects.requireNonNull(key); @@ -153,7 +158,8 @@ public V get(final Object key) @SuppressWarnings("unchecked") public V put(final Object key, final Object value) { - requireNonNull(value, "Value cannot be null"); + final Object val = mapNullValue(value); + requireNonNull(val, "Value cannot be null"); final Object[] entries = this.entries; final int mask = entries.length - 1; @@ -177,11 +183,11 @@ public V put(final Object key, final Object value) entries[index] = key; } - entries[index + 1] = value; + entries[index + 1] = val; increaseCapacity(); - return (V)oldValue; + return unmapNullValue(oldValue); } private void increaseCapacity() @@ -231,15 +237,16 @@ private void rehash(final int newCapacity) */ public boolean containsValue(final Object value) { + final Object val = mapNullValue(value); boolean found = false; - if (value != null) + if (val != null) { final Object[] entries = this.entries; final int length = entries.length; for (int valueIndex = 1; valueIndex < length; valueIndex += 2) { - if (value == entries[valueIndex] || value.equals(entries[valueIndex])) + if (val == entries[valueIndex] || val.equals(entries[valueIndex])) { found = true; break; @@ -285,7 +292,7 @@ public void forEach(final BiConsumer consumer) { if (entries[keyIndex + 1] != null) { - consumer.accept((K)entries[keyIndex], (V)entries[keyIndex + 1]); + consumer.accept((K)entries[keyIndex], unmapNullValue(entries[keyIndex + 1])); } } } @@ -295,7 +302,7 @@ public void forEach(final BiConsumer consumer) */ public boolean containsKey(final Object key) { - return get(key) != null; + return getMapped(key) != null; } /** @@ -376,7 +383,7 @@ public V remove(final Object key) keyIndex = next(keyIndex, mask); } - return (V)oldValue; + return unmapNullValue(oldValue); } @SuppressWarnings("FinalParameters") @@ -426,7 +433,7 @@ public String toString() while (true) { entryIterator.next(); - sb.append(entryIterator.getKey()).append('=').append(entryIterator.getValue()); + sb.append(entryIterator.getKey()).append('=').append(unmapNullValue(entryIterator.getValue())); if (!entryIterator.hasNext()) { return sb.append('}').toString(); @@ -462,6 +469,16 @@ public int hashCode() return entrySet().hashCode(); } + protected Object mapNullValue(final Object value) + { + return value; + } + + protected V unmapNullValue(final Object value) + { + return (V)value; + } + private static int next(final int index, final int mask) { return (index + 2) & mask; @@ -592,7 +609,7 @@ public V next() { findNext(); - return (V)entries[keyPosition() + 1]; + return unmapNullValue(entries[keyPosition() + 1]); } } @@ -609,26 +626,28 @@ public K getKey() @SuppressWarnings("unchecked") public V getValue() { - return (V)entries[keyPosition() + 1]; + return unmapNullValue(entries[keyPosition() + 1]); } @SuppressWarnings("unchecked") public V setValue(final V value) { + final V val = (V)mapNullValue(value); + if (!isPositionValid) { throw new IllegalStateException(); } - if (null == value) + if (null == val) { throw new IllegalArgumentException(); } final int keyPosition = keyPosition(); final Object prevValue = entries[keyPosition + 1]; - entries[keyPosition + 1] = value; - return (V)prevValue; + entries[keyPosition + 1] = val; + return unmapNullValue(prevValue); } public Entry next() @@ -667,7 +686,8 @@ public V setValue(final V value) public int hashCode() { - return getKey().hashCode() ^ getValue().hashCode(); + final V v = getValue(); + return getKey().hashCode() ^ (v != null ? v.hashCode() : 0); } public boolean equals(final Object o) @@ -679,8 +699,8 @@ public boolean equals(final Object o) final Entry e = (Entry)o; - return (e.getKey() != null && e.getValue() != null) && - (e.getKey().equals(k) && e.getValue().equals(v)); + return (e.getKey() != null && e.getKey().equals(k)) && + ((e.getValue() == null && v == null) || e.getValue().equals(v)); } public String toString() @@ -855,8 +875,8 @@ public void clear() public boolean contains(final Object o) { final Entry entry = (Entry)o; - final V value = get(entry.getKey()); - return value != null && value.equals(entry.getValue()); + final V value = getMapped(entry.getKey()); + return value != null && value.equals(mapNullValue(entry.getValue())); } } } diff --git a/agrona/src/main/java/org/agrona/generation/PrimitiveExpander.java b/agrona/src/main/java/org/agrona/generation/PrimitiveExpander.java index 91378cfc2..aa4e4aeed 100644 --- a/agrona/src/main/java/org/agrona/generation/PrimitiveExpander.java +++ b/agrona/src/main/java/org/agrona/generation/PrimitiveExpander.java @@ -45,6 +45,7 @@ public static void main(final String[] args) throws IOException expandPrimitiveSpecialisedClass(COLLECTIONS, "IntLruCache"); expandPrimitiveSpecialisedClass(COLLECTIONS, "Int2ObjectCache"); expandPrimitiveSpecialisedClass(COLLECTIONS, "Int2ObjectHashMap"); + expandPrimitiveSpecialisedClass(COLLECTIONS, "Int2NullableObjectHashMap"); expandPrimitiveSpecialisedClass(COLLECTIONS, "Object2IntHashMap"); } diff --git a/agrona/src/test/java/org/agrona/collections/Int2NullableObjectHashMapConformanceTest.java b/agrona/src/test/java/org/agrona/collections/Int2NullableObjectHashMapConformanceTest.java new file mode 100644 index 000000000..0342b213a --- /dev/null +++ b/agrona/src/test/java/org/agrona/collections/Int2NullableObjectHashMapConformanceTest.java @@ -0,0 +1,87 @@ +package org.agrona.collections; + +import com.google.common.collect.testing.Helpers; +import com.google.common.collect.testing.MapTestSuiteBuilder; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.TestMapGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.MapFeature; +import junit.framework.TestSuite; + +import java.util.List; +import java.util.Map; + + +public class Int2NullableObjectHashMapConformanceTest +{ + // Generated suite to test conformity to the java.util.Set interface + public static TestSuite suite() + { + return mapTestSuite(new TestMapGenerator() + { + public Integer[] createKeyArray(final int length) + { + return new Integer[length]; + } + + public Integer[] createValueArray(final int length) + { + return new Integer[length]; + } + + public SampleElements> samples() + { + return new SampleElements<>( + Helpers.mapEntry(1, 123), + Helpers.mapEntry(2, 234), + Helpers.mapEntry(3, 345), + Helpers.mapEntry(345, 6), + Helpers.mapEntry(777, 666)); + } + + public Map create(final Object... entries) + { + final Int2NullableObjectHashMap map = new Int2NullableObjectHashMap( + entries.length * 2, Hashing.DEFAULT_LOAD_FACTOR, false); + + for (final Object o : entries) + { + @SuppressWarnings("unchecked") + final Map.Entry e = (Map.Entry)o; + map.put(e.getKey(), e.getValue()); + } + + return map; + } + + @SuppressWarnings("unchecked") + public Map.Entry[] createArray(final int length) + { + return new Map.Entry[length]; + } + + public Iterable> order(final List> insertionOrder) + { + return insertionOrder; + } + }, Int2NullableObjectHashMap.class.getSimpleName()); + } + + private static TestSuite mapTestSuite(final TestMapGenerator testMapGenerator, final String name) + { + return new MapTestSuiteBuilder() + { + { + usingGenerator(testMapGenerator); + } + }.withFeatures( + MapFeature.GENERAL_PURPOSE, + MapFeature.ALLOWS_NULL_VALUES, + MapFeature.ALLOWS_NULL_VALUE_QUERIES, + CollectionSize.ANY, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE) + .named(name) + .createTestSuite(); + } +} \ No newline at end of file diff --git a/agrona/src/test/java/org/agrona/collections/Int2ObjectHashMapTest.java b/agrona/src/test/java/org/agrona/collections/Int2ObjectHashMapTest.java index e0a1dbbbe..1b6c76ea9 100644 --- a/agrona/src/test/java/org/agrona/collections/Int2ObjectHashMapTest.java +++ b/agrona/src/test/java/org/agrona/collections/Int2ObjectHashMapTest.java @@ -24,6 +24,8 @@ import java.util.Map; import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.number.OrderingComparison.lessThan; @@ -375,5 +377,54 @@ public void shouldCopyConstructAndBeEqual() final Int2ObjectHashMap mapCopy = new Int2ObjectHashMap<>(intToObjectMap); assertThat(mapCopy, is(intToObjectMap)); } + + @Test + public void shouldAllowNullValuesWithNullMapping() + { + final Int2ObjectHashMap map = new Int2ObjectHashMap() + { + private final Object nullRef = new Object(); + + @Override + protected Object mapNullValue(final Object value) + { + return value == null ? nullRef : value; + } + + @Override + protected String unmapNullValue(final Object value) + { + return value == nullRef ? null : (String)value; + } + }; + + map.put(0, null); + map.put(1, "one"); + + assertThat(map.get(0), nullValue()); + assertThat(map.get(1), is("one")); + assertThat(map.get(-1), nullValue()); + + assertThat(map.containsKey(0), is(true)); + assertThat(map.containsKey(1), is(true)); + assertThat(map.containsKey(-1), is(false)); + + assertThat(map.values(), containsInAnyOrder(null, "one")); + assertThat(map.keySet(), containsInAnyOrder(0, 1)); + + assertThat(map.size(), is(2)); + + map.remove(0); + + assertThat(map.get(0), nullValue()); + assertThat(map.get(1), is("one")); + assertThat(map.get(-1), nullValue()); + + assertThat(map.containsKey(0), is(false)); + assertThat(map.containsKey(1), is(true)); + assertThat(map.containsKey(-1), is(false)); + + assertThat(map.size(), is(1)); + } } diff --git a/agrona/src/test/java/org/agrona/collections/Long2NullableObjectHashMapConformanceTest.java b/agrona/src/test/java/org/agrona/collections/Long2NullableObjectHashMapConformanceTest.java new file mode 100644 index 000000000..2a7bbf762 --- /dev/null +++ b/agrona/src/test/java/org/agrona/collections/Long2NullableObjectHashMapConformanceTest.java @@ -0,0 +1,100 @@ +package org.agrona.collections; + +import com.google.common.collect.testing.Helpers; +import com.google.common.collect.testing.MapTestSuiteBuilder; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.TestMapGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.MapFeature; +import junit.framework.TestSuite; + +import java.util.List; +import java.util.Map; + + +public class Long2NullableObjectHashMapConformanceTest +{ + // Generated suite to test conformity to the java.util.Set interface + public static TestSuite suite() + { + return mapTestSuite(new TestMapGenerator() + { + public Long[] createKeyArray(final int length) + { + return new Long[length]; + } + + public Long[] createValueArray(final int length) + { + return new Long[length]; + } + + public SampleElements> samples() + { + return new SampleElements<>( + Helpers.mapEntry(1L, 123L), + Helpers.mapEntry(2L, 234L), + Helpers.mapEntry(3L, 345L), + Helpers.mapEntry(345L, 6L), + Helpers.mapEntry(777L, 666L)); + } + + public Map create(final Object... entries) + { + final Long2NullableObjectHashMap map = new Long2NullableObjectHashMap( + entries.length * 2, Hashing.DEFAULT_LOAD_FACTOR, false) + { + private final Object nullRef = new Object(); + + protected Object mapNullValue(final Object value) + { + return value == null ? nullRef : value; + } + + protected Long unmapNullValue(final Object value) + { + return value == nullRef ? null : (Long)value; + } + }; + + for (final Object o : entries) + { + @SuppressWarnings("unchecked") + final Map.Entry e = (Map.Entry)o; + map.put(e.getKey(), e.getValue()); + } + + return map; + } + + @SuppressWarnings("unchecked") + public Map.Entry[] createArray(final int length) + { + return new Map.Entry[length]; + } + + public Iterable> order(final List> insertionOrder) + { + return insertionOrder; + } + }, Long2NullableObjectHashMap.class.getSimpleName()); + } + + private static TestSuite mapTestSuite(final TestMapGenerator testMapGenerator, final String name) + { + return new MapTestSuiteBuilder() + { + { + usingGenerator(testMapGenerator); + } + }.withFeatures( + MapFeature.GENERAL_PURPOSE, + MapFeature.ALLOWS_NULL_VALUES, + MapFeature.ALLOWS_NULL_VALUE_QUERIES, + CollectionSize.ANY, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE) + .named(name) + .createTestSuite(); + } +} \ No newline at end of file diff --git a/agrona/src/test/java/org/agrona/collections/Object2NullableObjectHashMapConformanceTest.java b/agrona/src/test/java/org/agrona/collections/Object2NullableObjectHashMapConformanceTest.java new file mode 100644 index 000000000..3beca649c --- /dev/null +++ b/agrona/src/test/java/org/agrona/collections/Object2NullableObjectHashMapConformanceTest.java @@ -0,0 +1,87 @@ +package org.agrona.collections; + +import com.google.common.collect.testing.Helpers; +import com.google.common.collect.testing.MapTestSuiteBuilder; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.TestMapGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.MapFeature; +import junit.framework.TestSuite; + +import java.util.List; +import java.util.Map; + + +public class Object2NullableObjectHashMapConformanceTest +{ + // Generated suite to test conformity to the java.util.Set interface + public static TestSuite suite() + { + return mapTestSuite(new TestMapGenerator() + { + public Long[] createKeyArray(final int length) + { + return new Long[length]; + } + + public Long[] createValueArray(final int length) + { + return new Long[length]; + } + + public SampleElements> samples() + { + return new SampleElements<>( + Helpers.mapEntry(1L, 123L), + Helpers.mapEntry(2L, 234L), + Helpers.mapEntry(3L, 345L), + Helpers.mapEntry(345L, 6L), + Helpers.mapEntry(777L, 666L)); + } + + public Map create(final Object... entries) + { + final Object2NullableObjectHashMap map = new Object2NullableObjectHashMap<>( + entries.length * 2, Hashing.DEFAULT_LOAD_FACTOR, false); + + for (final Object o : entries) + { + @SuppressWarnings("unchecked") + final Map.Entry e = (Map.Entry)o; + map.put(e.getKey(), e.getValue()); + } + + return map; + } + + @SuppressWarnings("unchecked") + public Map.Entry[] createArray(final int length) + { + return new Map.Entry[length]; + } + + public Iterable> order(final List> insertionOrder) + { + return insertionOrder; + } + }, Object2NullableObjectHashMap.class.getSimpleName()); + } + + private static TestSuite mapTestSuite(final TestMapGenerator testMapGenerator, final String name) + { + return new MapTestSuiteBuilder() + { + { + usingGenerator(testMapGenerator); + } + }.withFeatures( + MapFeature.GENERAL_PURPOSE, + MapFeature.ALLOWS_NULL_VALUES, + MapFeature.ALLOWS_NULL_VALUE_QUERIES, + CollectionSize.ANY, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE) + .named(name) + .createTestSuite(); + } +} \ No newline at end of file