A HashSet “lost” my object after I changed one field. Walked through it in jshell
I wanted to see this one with my own eyes, so I typed it into jshell. Output is below with notes in between.
jshell> class Point {
...> int x;
...> Point(int x) { this.x = x; }
...> public boolean equals(Object o) {
...> return o instanceof Point p && p.x == x;
...> }
...> u/Override public int hashCode() { return Integer.hashCode(x); }
...> }
jshell> var set = new HashSet<Point>();
jshell> var p = new Point(1);
jshell> set.add(p);
$4 ==> true
Nothing odd yet. equals and hashCode both use x, which is what most IDEs generate.
jshell> p.x = 2;
jshell> set.size()
$6 ==> 1
jshell> set.contains(p)
$7 ==> false
jshell> set.remove(p)
$8 ==> false
This is the part that surprised me. The set says it has one element, and it says it doesn’t contain the exact object I’m holding a reference to. remove fails too.
jshell> for (Point q : set) System.out.println(q == p);
true
Iteration finds it. Iteration just walks every bucket, no hashing involved. contains and remove compute hashCode() first (now based on 2), go to that bucket, and find nothing. The object is still sitting in the bucket picked when its x was 1.
As far as I understand it, there’s no error because HashSet never learns the object changed. It only looks at the hash at insertion time and again at lookup time. If the two differ, you get a silent miss.
So the object is still in the set but unreachable by key. That’s the part that would be annoying to debug in a real codebase: size is right, iteration is right, lookups are wrong.
Things I’ve seen suggested for this:
- keep fields used in
hashCodefinal (records do this by default) - if the key has to change, remove it first, change it, add it again
- use an immutable key type, like a String or a Long id, instead of the whole entity
Question for people who’ve worked on bigger projects: do you ever see this with JPA entities used as keys in sets, where the id is null before persist and set afterward? I’d guess it’s the same problem in a different outfit, but I haven’t tried it.