hashCode and equals Contract
IntermediateCorrectly implementing hashCode and equals is critical for objects used as HashMap keys or in HashSets — violating the contract causes subtle bugs.
Overview
The equals/hashCode contract is one of the most important rules in Java: if two objects are equal (equals() returns true), they MUST have the same hashCode. The reverse is not required — hash collisions are allowed. Violating this contract causes objects to "disappear" in HashMaps and HashSets. Java 7+ provides Objects.hash() and Objects.equals() as utilities. Java records automatically generate correct implementations.
The equals/hashCode Contract
equals() must be: reflexive (a.equals(a)), symmetric (a.equals(b) == b.equals(a)), transitive, consistent, and a.equals(null) == false.
hashCode() contract: (1) consistent across calls, (2) if a.equals(b) then a.hashCode() == b.hashCode(). Note: a.hashCode() == b.hashCode() does NOT imply a.equals(b) — collisions are fine.
// BROKEN — equals without hashCode
public class BrokenPoint {
int x, y;
@Override
public boolean equals(Object o) {
if (!(o instanceof BrokenPoint p)) return false;
return x == p.x && y == p.y;
}
// hashCode not overridden — uses Object's identity hash
}
BrokenPoint p1 = new BrokenPoint(1, 2);
BrokenPoint p2 = new BrokenPoint(1, 2);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // false (probably)
Set<BrokenPoint> set = new HashSet<>();
set.add(p1);
set.contains(p2); // FALSE — looks in wrong bucket!
Map<BrokenPoint, String> map = new HashMap<>();
map.put(p1, "origin");
map.get(p2); // NULL — same bugCorrect Implementation
Use all fields that participate in equals in hashCode too. Objects.hash() combines multiple fields cleanly. For performance-critical code, cache the hash if the object is immutable.
Java 14+ records automatically generate correct equals and hashCode based on all components.
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // same reference — fast path
if (!(o instanceof Point p)) return false; // null + type check
return x == p.x && y == p.y; // field comparison
}
@Override
public int hashCode() {
return Objects.hash(x, y); // combines fields with 31* polynomial
}
}
// Cached hashCode for immutable objects (String-style)
public final class ImmutableKey {
private final String a;
private final int b;
private int cachedHash; // 0 means not computed
@Override
public int hashCode() {
int h = cachedHash;
if (h == 0) {
h = Objects.hash(a, b);
cachedHash = h;
}
return h;
}
}
// Records — automatic correct implementation
record Point(int x, int y) {} // equals, hashCode, toString all generatedHashMap Internals and Hash Collisions
HashMap uses hashCode to find the bucket (array index = hash & (capacity-1)), then equals to find the exact key within the bucket. Collisions place multiple entries in the same bucket as a linked list; Java 8 converts to a tree (Red-Black) when a bucket exceeds 8 entries.
A poor hashCode that returns a constant causes every key to collide — HashMap degrades to O(n) linked list lookups.
// HashMap lookup process
// 1. Compute key.hashCode()
// 2. Spread bits: (h = key.hashCode()) ^ (h >>> 16)
// 3. bucket = hash & (table.length - 1)
// 4. Walk bucket (list or tree) using key.equals()
// Good hashCode — distributes evenly
// Objects.hash() uses: s[0]*31^(n-1) + s[1]*31^(n-2) + ...
// 31 is prime, distributes well, easily JIT-optimised
// BAD hashCode — constant = all keys in one bucket = O(n) lookups
@Override public int hashCode() { return 42; } // NEVER do this
// BAD hashCode — only x = collisions for all points with same x
@Override public int hashCode() { return x; }
// Verifying distribution
Map<Integer, Long> distribution = IntStream.range(0, 10_000)
.mapToObj(i -> new Point(i % 100, i / 100))
.collect(Collectors.groupingBy(
p -> (p.hashCode() & 0x7FFFFFFF) % 16,
Collectors.counting()));
// Ideally: ~625 entries per bucket (10000 / 16)Key Points to Remember
- equals() true → hashCode() must be equal. hashCode() equal does NOT imply equals() true.
- Always override hashCode when you override equals — IDEs and Lombok do this automatically.
- Use Objects.hash(field1, field2, ...) for a clean, collision-resistant hashCode.
- Cache hashCode in immutable objects for performance (see String).
- Bad hashCode (e.g. constant) degrades HashMap to O(n) — evenly distributing hashes is important.
Practice hashCode and equals Contract in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is the hashCode/equals contract in Java?
What happens if you override equals but not hashCode?
How does HashMap use hashCode and equals internally?
Why does Java 8 convert HashMap bucket lists to trees?
Can two objects have the same hashCode but not be equal?
Ask Aria about hashCode and equals Contract
Your personal AI tutor — ask anything about this concept