SSerguey Asael Shinder
Java coding notes: the JVM, and writing software that lasts

Serguey Asael Shinder: Change a key after you put it in a HashMap and the map may never find it again

· by Serguey Asael Shinder / Serguey Shinder

Most of us learn that a HashMap key needs equals and hashCode. Fewer of us notice the second half of the deal: the key must not change in a way that affects them while it is in the map.

What the contract says. The Map javadoc puts it as a note: "great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map." The Set javadoc has the same warning for set elements.

Why it breaks. The Object.hashCode contract) only promises a consistent hash code "provided no information used in equals comparisons on the object is modified". A hash map stores an entry according to the hash code the key had when it was inserted. Change a field that feeds hashCode, and the key now hashes somewhere else, while the entry is still filed under the old value:

record Point(int x, int y) {}                 // immutable: safe as a key
final class MutablePoint {
    int x, y;
    MutablePoint(int x, int y) { this.x = x; this.y = y; }
    @Override public boolean equals(Object o) {
        return o instanceof MutablePoint p && p.x == x && p.y == y;
    }
    @Override public int hashCode() { return 31 * x + y; }
}
Map<MutablePoint, String> labels = new HashMap<>();
MutablePoint p = new MutablePoint(1, 2);
labels.put(p, "start");
p.x = 5;                                       // key changed while in the map
labels.get(p);                                 // the javadoc promises nothing here
labels.containsKey(new MutablePoint(1, 2));    // nor here
Serguey Asael Shinder: Change a key after you put it in a HashMap and the map may never find it again
Change a key after you put it in a HashMap and the map may never find it again — Serguey Asael Shinder

The javadoc does not say which answer you get, and that is the point: "not specified" means a lookup can miss, the entry can still show up when you iterate, and size() keeps counting it. The usual symptom is a map that holds an entry you cannot reach by key and cannot remove by key.

What to write instead. Use immutable keys. A record with immutable components gives you equals and hashCode derived from fields that cannot change after construction, which is exactly what a key needs. If a key has to change, take the entry out first and put it back after:

String label = labels.remove(p);   // remove while the hash code is still the old one
p.x = 5;
labels.put(p, label);

The rule I take from it. Whatever goes into equals and hashCode must be frozen for as long as the object sits in a hash-based collection. If you cannot promise that, the object is not a key.

No JDK runs on the machine this note was written on; the code is written to compile against Java 21 and the behaviour described is the one stated in the linked javadoc.