Serguey Asael Shinder: Change a key after you put it in a HashMap and the map may never find it again
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

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.