If you override equals but not hashCode, hash-based collection lookups silently fail. Equal objects land in different buckets because they inherit Object.hashCode's identity-based hash, so HashMap.get and HashSet.contains can't find them.
The bug in action
class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
return o instanceof Point p && p.x == x && p.y == y;
}
// hashCode NOT overridden — inherits Object.hashCode (identity)
}
var set = new HashSet<Point>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2)); // false (!)
set.size(); // 1
set.add(new Point(1, 2)); // adds again
set.size(); // 2 — duplicates!
Two Point(1, 2) instances are .equals to each other, but Object.hashCode returns a different int for each (it's based on the object's memory address / identity). So they hash to different buckets, and HashSet never even gets to call equals.
What HashMap.put does, step by step
put(key, value): h = key.hashCode() <- identity hash, different per instance bucket = h & (table.length - 1) <- different bucket each time walk bucket, compare with equals <- bucket is empty, no comparison insert new entry <- duplicate!
Why the JDK can't catch this for you
Object.hashCode returns a valid int. The compiler can't know your override of equals is semantically incompatible with the inherited hashCode — both methods exist and type-check. IDEs and linters (SpotBugs, Error Prone's EqualsHashCode) detect it; the language doesn't.
Effect on each collection type
HashSet— duplicates accumulate.HashMap—get(key)returns null right afterput(key, v).LinkedHashMap— same as HashMap, plus iteration order is broken.ConcurrentHashMap— same plus race conditions during resize.TreeSet/TreeMap— unaffected; they usecompareToorComparator, not hashCode.ArrayList/LinkedList— unaffected;containsdoes linear scan withequals.