Java › equals(), hashCode() & toString() Contracts
Why must hashCode() be overridden whenever equals() is overridden, and what specific, hard-to-diagnose bugs occur in a HashMap when this contract is violated (equals() overridden but hashCode() left as default)?
AdvancedAnswer
The equals()/hashCode() contract requires that equal objects (per equals()) MUST produce the same hashCode() value -- if you override equals() to compare field values but leave hashCode() as Object's default identity-based implementation, two logically-equal instances will very likely produce different hash codes, causing a HashMap to place them in different buckets; the practical symptom is that map.get(key) or map.containsKey(key) can return null/false for a key that IS logically present (constructed as a separate but equal instance), since the lookup uses hashCode() first to locate the right bucket before ever calling equals() to confirm a match -- a notoriously confusing bug because the map appears to have silently "lost" or "rejected" data that was, in fact, correctly inserted.
public class BadKey {
private final String id;
public BadKey(String id) { this.id = id; }
@Override
public boolean equals(Object o) { // overridden
return o instanceof BadKey k && id.equals(k.id);
}
// hashCode() NOT overridden -- still uses Object's identity-based default!
}
Map<BadKey, String> map = new HashMap<>();
map.put(new BadKey("x"), "value");
System.out.println(map.get(new BadKey("x"))); // prints null! Different hashCode() despite equals() returning trueReal-world example A caching layer using a custom composite key class that overrode equals() but forgot hashCode() experienced intermittent cache misses that looked exactly like cache entries randomly disappearing, actually caused by every lookup using a fresh key instance landing in a different bucket than the one used during insertion, undetectable without understanding this specific contract violation.