diff --git a/src/org/sosy_lab/common/collect/PathCopyingPersistentTreeMap.java b/src/org/sosy_lab/common/collect/PathCopyingPersistentTreeMap.java index 561e4b2b0..74bb8c597 100644 --- a/src/org/sosy_lab/common/collect/PathCopyingPersistentTreeMap.java +++ b/src/org/sosy_lab/common/collect/PathCopyingPersistentTreeMap.java @@ -54,7 +54,7 @@ * always compares according to the natural ordering. All methods may throw {@link * ClassCastException} is key objects are passed that do not implement {@link Comparable}. * - *

The natural ordering of the keys needs to be consistent with equals. + *

The natural ordering of the keys needs to be consistent with equals and object identity. * *

As for all {@link PersistentMap}s, all collection views and all iterators are immutable. They * do not reflect changes made to the map and all their modifying operations throw {@link @@ -137,6 +137,14 @@ Node withColor(boolean color) { } } + Node withValue(V newValue) { + if (newValue == getValue()) { + return this; + } else { + return new Node<>(getKey(), newValue, left, right, isRed); + } + } + @SuppressWarnings("ReferenceEquality") // cannot use equals() for check whether tree is the same Node withLeftChild(Node newLeft) { if (newLeft == left) { @@ -555,7 +563,14 @@ private static , V> Node putAndCopy0( current = current.withRightChild(newRight); } else { - current = new Node<>(key, value, current.left, current.right, current.getColor()); + // This always keeps the old (equal) key object. This has useful implications: + // Because we reuse the old key object, the key instance does not change and potential `==` + // comparisons on the key at other locations still work successfully. + // We do always use the new value object; but in case that the new value object is identical + // to the old value object, we can reuse the existing Node object and the whole map. This also + // enables `==` comparisons to succeed and saves some memory. + // This behavior also matches what JDK maps do. + current = current.withValue(value); } // restore invariants diff --git a/src/org/sosy_lab/common/collect/PathCopyingPersistentTreeMapTest.java b/src/org/sosy_lab/common/collect/PathCopyingPersistentTreeMapTest.java index 7a425f792..ab8bf97af 100644 --- a/src/org/sosy_lab/common/collect/PathCopyingPersistentTreeMapTest.java +++ b/src/org/sosy_lab/common/collect/PathCopyingPersistentTreeMapTest.java @@ -12,6 +12,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; +import com.google.common.collect.FluentIterable; import com.google.common.collect.Ordering; import com.google.common.collect.testing.NavigableMapTestSuiteBuilder; import com.google.common.collect.testing.TestStringSortedMapGenerator; @@ -21,6 +22,7 @@ import com.google.common.testing.EqualsTester; import com.google.errorprone.annotations.Var; import java.util.Collection; +import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; import java.util.Random; @@ -434,4 +436,96 @@ public void testEntrySetContains() { assertThat(second.entrySet().containsAll(first.entrySet())).isFalse(); assertThat(first.entrySet().containsAll(second.entrySet())).isFalse(); } + + @Test + public void testRemovingMissingKey() { + map = map.putAndCopy("a", "").putAndCopy("b", "").putAndCopy("y", "").putAndCopy("z", ""); + + assertWithMessage("Removing missing key should produce same map") + .that(map.removeAndCopy("key")) + .isSameInstanceAs(map); + } + + @Test + @SuppressWarnings({"checkstyle:IllegalInstantiation", "StringUselessMethods"}) + public void testSettingIdenticalObjects() { + String k1 = new String("key"); + String k2 = new String("key"); + String v1 = new String("value"); + String v2 = new String("value"); + map = + map.putAndCopy("a", "") + .putAndCopy("b", "") + .putAndCopy(k1, v1) + .putAndCopy("y", "") + .putAndCopy("z", ""); + + assertWithMessage("Reinserting same k/v pair should produce same map") + .that(map.putAndCopy(k1, v1)) + .isSameInstanceAs(map); + + assertWithMessage("Reinserting same value should produce same map") + .that(map.putAndCopy(k2, v1)) + .isSameInstanceAs(map); + + assertWithMessage("Inserting new value should produce map with new value") + .that(map.putAndCopy(k1, v2).get(k1)) + .isSameInstanceAs(v2); + + assertWithMessage("Inserting new k/v pair should keep old key") + .that( + FluentIterable.from(map.putAndCopy(k2, v2).keySet()) + .filter(s -> s.length() > 1) + .first() + .get()) + .isSameInstanceAs(k1); + assertWithMessage("Inserting new k/v pair should produce map with new value") + .that(map.putAndCopy(k2, v2).get(k2)) + .isSameInstanceAs(v2); + } + + @Test + public void testSettingIdenticalObjectsInHashMap() { + testSettingIdenticalObjectsInStandardMap(new HashMap<>()); + } + + @Test + public void testSettingIdenticalObjectsInTreeMap() { + testSettingIdenticalObjectsInStandardMap(new TreeMap<>()); + } + + @SuppressWarnings({"checkstyle:IllegalInstantiation", "StringUselessMethods"}) + private static void testSettingIdenticalObjectsInStandardMap(Map map) { + // not testing own code, but checking expectations of other map implementations + + String k1 = new String("key"); + String k2 = new String("key"); + String k3 = new String("key"); + String v1 = new String("value"); + String v2 = new String("value"); + String v3 = new String("value"); + map.put("a", ""); + map.put("b", ""); + map.put(k1, v1); + map.put("y", ""); + map.put("z", ""); + + map.put(k2, v1); + assertWithMessage("Inserting same value should keep old key") + .that(FluentIterable.from(map.keySet()).filter(s -> s.length() > 1).first().get()) + .isSameInstanceAs(k1); + + map.put(k1, v2); + assertWithMessage("Inserting new value should produce map with new value") + .that(map.get(k1)) + .isSameInstanceAs(v2); + + map.put(k3, v3); + assertWithMessage("Inserting new k/v pair should keep old key") + .that(FluentIterable.from(map.keySet()).filter(s -> s.length() > 1).first().get()) + .isSameInstanceAs(k1); + assertWithMessage("Inserting new k/v pair should produce map with new value") + .that(map.get(k1)) + .isSameInstanceAs(v3); + } } diff --git a/src/org/sosy_lab/common/collect/PersistentMap.java b/src/org/sosy_lab/common/collect/PersistentMap.java index 4bb3ca2dd..88ffbfc1e 100644 --- a/src/org/sosy_lab/common/collect/PersistentMap.java +++ b/src/org/sosy_lab/common/collect/PersistentMap.java @@ -20,8 +20,8 @@ /** * Interface for persistent map. A persistent data structure is immutable, but provides cheap * copy-and-write operations. Thus all write operations ({{@link #putAndCopy(Object, Object)}, - * {{@link #removeAndCopy(Object)}}) will not modify the current instance, but return a new instance - * instead. + * {{@link #removeAndCopy(Object)}}) will not modify the current instance, but return an updated + * instance instead. * *

All modifying operations inherited from {@link Map} are not supported and will always throw * {@link UnsupportedOperationException}. All collections returned by methods of this interface are @@ -35,11 +35,11 @@ @Immutable(containerOf = {"K", "V"}) public interface PersistentMap extends Map { - /** Replacement for {{@link #put(Object, Object)} that returns a fresh instance. */ + /** Replacement for {{@link #put(Object, Object)} that returns an updated map. */ @CheckReturnValue PersistentMap putAndCopy(@CompatibleWith("K") K key, @CompatibleWith("V") V value); - /** Replacement for {{@link #remove(Object)} that returns a fresh instance. */ + /** Replacement for {{@link #remove(Object)} that returns an updated map. */ @CheckReturnValue PersistentMap removeAndCopy(@CompatibleWith("K") Object key);