Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 72 additions & 10 deletions src/java.base/share/classes/java/util/IdentityHashMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
* designed for use only in the rare cases wherein reference-equality
* semantics are required.</b>
*
* <p>The view collections of this map also have reference-equality semantics
* for their elements. See the {@link keySet() keySet}, {@link values() values},
* and {@link entrySet() entrySet} methods for further information.
*
* <p>A typical use of this class is <i>topology-preserving object graph
* transformations</i>, such as serialization or deep-copying. To perform such
* a transformation, a program must maintain a "node table" that keeps track
Expand Down Expand Up @@ -346,7 +350,8 @@ public V get(Object key) {

/**
* Tests whether the specified object reference is a key in this identity
* hash map.
* hash map. Returns {@code true} if and only if this map contains a mapping
* with key {@code k} such that {@code (key == k)}.
*
* @param key possible key
* @return {@code true} if the specified object reference is a key
Expand All @@ -370,7 +375,8 @@ public boolean containsKey(Object key) {

/**
* Tests whether the specified object reference is a value in this identity
* hash map.
* hash map. Returns {@code true} if and only if this map contains a mapping
* with value {@code v} such that {@code (value == v)}.
*
* @param value value whose presence in this map is to be tested
* @return {@code true} if this map maps one or more keys to the
Expand Down Expand Up @@ -411,8 +417,9 @@ private boolean containsMapping(Object key, Object value) {

/**
* Associates the specified value with the specified key in this identity
* hash map. If the map previously contained a mapping for the key, the
* old value is replaced.
* hash map. If this map already {@link containsKey(Object) contains}
* a mapping for the key, the old value is replaced, otherwise, a new mapping
* is inserted into 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
Expand Down Expand Up @@ -497,8 +504,10 @@ private boolean resize(int newCapacity) {

/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
* For each mapping in the specified map, if this map already
* {@link containsKey(Object) contains} a mapping for the key,
* its value is replaced with the value from the specified map;
* otherwise, a new mapping is inserted into this map.
*
* @param m mappings to be stored in this map
* @throws NullPointerException if the specified map is null
Expand All @@ -516,6 +525,8 @@ public void putAll(Map<? extends K, ? extends V> m) {

/**
* Removes the mapping for this key from this map if present.
* The mapping is removed if and only if the mapping has a key
* {@code k} such that (key == k).
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with {@code key}, or
Expand Down Expand Up @@ -632,7 +643,9 @@ public void clear() {
* {@code true} if the given object is also a map and the two maps
* represent identical object-reference mappings. More formally, this
* map is equal to another map {@code m} if and only if
* {@code this.entrySet().equals(m.entrySet())}.
* {@code this.entrySet().equals(m.entrySet())}. See the
* {@link entrySet() entrySet} method for the specification of equality
* of this map's entries.
*
* <p><b>Owing to the reference-equality-based semantics of this map it is
* possible that the symmetry and transitivity requirements of the
Expand Down Expand Up @@ -667,8 +680,11 @@ public boolean equals(Object o) {

/**
* Returns the hash code value for this map. The hash code of a map is
* defined to be the sum of the hash codes of each entry in the map's
* {@code entrySet()} view. This ensures that {@code m1.equals(m2)}
* defined to be the sum of the hash codes of each entry of this map.
* See the {@link entrySet() entrySet} method for a specification of the
* hash code of this map's entries.
*
* <p>This specification ensures that {@code m1.equals(m2)}
* implies that {@code m1.hashCode()==m2.hashCode()} for any two
* {@code IdentityHashMap} instances {@code m1} and {@code m2}, as
* required by the general contract of {@link Object#hashCode}.
Expand Down Expand Up @@ -1162,7 +1178,9 @@ public Spliterator<V> spliterator() {
* e.getValue()==o.getValue()}. To accommodate these equals
* semantics, the {@code hashCode} method returns
* {@code System.identityHashCode(e.getKey()) ^
* System.identityHashCode(e.getValue())}.
* System.identityHashCode(e.getValue())}. (While the keys and values
* are compared using reference equality, the {@code Map.Entry}
* objects themselves are not.)
*
* <p><b>Owing to the reference-equality-based semantics of the
* {@code Map.Entry} instances in the set returned by this method,
Expand Down Expand Up @@ -1382,6 +1400,50 @@ public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
}
}

/**
* {@inheritDoc}
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key == k)}
* and {@code (value == v)}, then this method removes the mapping
* for this key and returns {@code true}; otherwise it returns
* {@code false}.
*/
@Override
public boolean remove(Object key, Object value) {
return removeMapping(key, value);
}

/**
* {@inheritDoc}
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key == k)}
* and {@code (oldValue == v)}, then this method associates
* {@code k} with {@code newValue} and returns {@code true};
* otherwise it returns {@code false}.
*/
@Override
public boolean replace(K key, V oldValue, V newValue) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);

while (true) {
Object item = tab[i];
if (item == k) {
if (tab[i + 1] != oldValue)
return false;
tab[i + 1] = newValue;
return true;
}
if (item == null)
return false;
i = nextKeyIndex(i, len);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately there's some mostly-duplicate code here. However, there's similar logic and code sprinkled throughout this class, so more duplication isn't necessarily the wrong thing to do. However, trying to unify this logic results in much more intrusive refactoring, which is harder to review, and which isn't backed up with tests (see JDK-8285295) which I wouldn't encourage pursuing right now. In other words, I'm ok with this duplicate logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On intrusive logic: I planned address it in https://bugs.openjdk.java.net/browse/JDK-8277520 (#6532), and if this one is integrated first, it may be possible to fix it up there.


/**
* Similar form as array-based Spliterators, but skips blank elements,
* and guestimates size as decreasing by half per split.
Expand Down
95 changes: 92 additions & 3 deletions test/jdk/java/util/IdentityHashMap/Basic.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

/*
* @test
* @bug 8285295
* @bug 8285295 8178355
* @summary Basic tests for IdentityHashMap
* @run testng Basic
*/
Expand All @@ -49,8 +49,6 @@
// the identities of Map.Entry instances obtained from the entrySet; however, the keys and
// values they contain are guaranteed to have the right identity.

// TODO remove(k, v)
// TODO replace(k, v1, v2)
// TODO add tests using null keys and values
// TODO deeper testing of view collections including iterators, equals, contains, etc.
// TODO Map.Entry::setValue
Expand Down Expand Up @@ -484,6 +482,97 @@ public void testRemoveKey() {
entry(k2, v2));
}

// remove(Object, Object) absent key, absent value
@Test
public void testRemoveAA() {
Box k1c = new Box(k1a);
Box v1c = new Box(v1a);
assertFalse(map.remove(k1c, v1c));
checkEntries(map.entrySet(),
entry(k1a, v1a),
entry(k1b, v1b),
entry(k2, v2));
}

// remove(Object, Object) absent key, present value
@Test
public void testRemoveAV() {
Box k1c = new Box(k1a);
assertFalse(map.remove(k1c, v1a));
checkEntries(map.entrySet(),
entry(k1a, v1a),
entry(k1b, v1b),
entry(k2, v2));
}

// remove(Object, Object) present key, absent value
@Test
public void testRemoveKA() {
Box v1c = new Box(v1a);
assertFalse(map.remove(k1a, v1c));
checkEntries(map.entrySet(),
entry(k1a, v1a),
entry(k1b, v1b),
entry(k2, v2));
}

// remove(Object, Object) present key, present value
@Test
public void testRemoveKV() {
assertTrue(map.remove(k1a, v1a));
checkEntries(map.entrySet(),
entry(k1b, v1b),
entry(k2, v2));
}

// replace(K, V, V) absent key, absent oldValue
@Test
public void testReplaceAA() {
Box k1c = new Box(k1a);
Box v1c = new Box(v1a);
Box newVal = new Box(v2);
assertFalse(map.replace(k1c, v1c, newVal));
checkEntries(map.entrySet(),
entry(k1a, v1a),
entry(k1b, v1b),
entry(k2, v2));
}

// replace(K, V, V) absent key, present oldValue
@Test
public void testReplaceAV() {
Box k1c = new Box(k1a);
Box newVal = new Box(v2);
assertFalse(map.replace(k1c, v1a, newVal));
checkEntries(map.entrySet(),
entry(k1a, v1a),
entry(k1b, v1b),
entry(k2, v2));
}

// replace(K, V, V) present key, absent oldValue
@Test
public void testReplaceKA() {
Box v1c = new Box(v1a);
Box newVal = new Box(v2);
assertFalse(map.replace(k1a, v1c, newVal));
checkEntries(map.entrySet(),
entry(k1a, v1a),
entry(k1b, v1b),
entry(k2, v2));
}

// replace(K, V, V) present key, present oldValue
@Test
public void testReplaceKV() {
Box newVal = new Box(v2);
assertTrue(map.replace(k1a, v1a, newVal));
checkEntries(map.entrySet(),
entry(k1a, newVal),
entry(k1b, v1b),
entry(k2, v2));
}

// AN: key absent, remappingFunction returns null
@Test
public void testComputeAN() {
Expand Down