What Happens If You Override equals() But Not hashCode()?
If you override equals() but forget about hashCode(), logically equal objects will have different hash codes. This breaks HashMap and HashSet.
🟢 Junior Level
If you override equals() but forget about hashCode(), logically equal objects will have different hash codes. This breaks HashMap and HashSet.
What happens:
public class User {
private Long id;
// equals() overridden, hashCode() — NOT
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return id != null && id.equals(user.id);
}
}
Set<User> set = new HashSet<>();
set.add(new User(1L)); // hashCode = identity_A → bucket X
set.add(new User(1L)); // hashCode = identity_B → bucket Y (different!)
System.out.println(set.size()); // 2! HashSet only checks equals inside a bucket.
// Buckets are DIFFERENT — equals is never even called.
Main rule: If you override equals — override hashCode too!
🟡 Middle Level
Practical Consequences
1. Data loss in HashMap:
objAis put in bucket #100 (its identity hash code)objB(equal to objA by equals) is searched in bucket #200 (different identity hash code)map.get(objB)returnsnull, even though the key exists
2. Duplicates in HashSet:
- Equal objects land in different buckets
- HashSet allows “duplicates” — business logic violation
3. Impossible removal:
map.remove(objB)searches in the wrong bucket- Object remains in memory — memory leak
Why This Is Hard to Debug
The error may not manifest:
- If objects aren’t used in hash collections
- If different hashCodes accidentally landed in the same bucket
- On the next resize, indices change — non-deterministic bug
How to Prevent
| Tool | What it does |
|---|---|
| SonarQube | Highlights as Critical Bug |
| SpotBugs | Warns about contract violation |
| IntelliJ IDEA | Warns during generation |
Lombok @EqualsAndHashCode |
Generates both methods |
| Java 14+ Records | Automatic generation |
🔴 Senior Level
Failure Mechanism at JVM Level
Object.hashCode() by default returns identity hash code — a number that the JVM associates with the object on the first call to System.identityHashCode(). It is NOT the memory address (though it may be derived from it) and does NOT change on GC moves. Two different instances of new User(1L) always have different identity hash codes, even if equals() returns true.
Heisenbug in Production
Heisenbug — a bug that manifests or disappears depending on conditions (named after Heisenberg’s uncertainty principle). In this case: in small tests, different hashCodes accidentally landed in the same bucket — test passes. In production with a large map — different buckets — bug manifests.
// In test (small map): different hashCodes landed in same bucket → works
// In production (large map, different resize): different buckets → crashes
This is one of the most insidious bugs: passes all tests, but crashes in production under certain conditions.
Memory Leak Pattern
Map<User, Data> cache = new HashMap<>();
cache.put(new User(1L), loadData()); // Bucket A
// Equal object searches in bucket B → null → loads again
cache.put(new User(1L), loadData()); // Another copy in bucket B
// Result: memory leak + data duplication
Security Implications
In systems with object-key caching, contract violation can lead to:
- Cache bypass (cache miss for equal keys)
- Sensitive data leakage (duplication in memory)
Best Practices
- If an object will be a key in a hash collection — generate both methods. Exception: identity semantics is intentional.
- Use an IDE — automatic generation of
equals()andhashCode() - Records (Java 14+) — ideal for keys. Records generate hashCode from ALL fields. If you have transient or computed fields — they’ll also be included in hashCode, which may be undesirable.
- Unit tests — verify the contract:
assertEquals(a.equals(b), a.hashCode() == b.hashCode() || !a.equals(b));
Lombok Pitfall
@EqualsAndHashCode // Generates both methods from ALL fields
class User {
Long id;
transient String cache; // Don't include transient/cache fields!
}
Use @EqualsAndHashCode(exclude = "cache").
🎯 Interview Cheat Sheet
Must know:
- Without hashCode(), equal objects land in DIFFERENT buckets (different identity hash codes)
- HashSet allows duplicates: size = 2 for two equal objects
- map.get() returns null for an equal object — searches in the wrong bucket
- Impossible remove() → memory leak (ghost entries)
- Heisenbug: may pass tests on small maps, crash in production
- One of the most insidious bugs — equal objects accidentally landed in the same bucket → test green
Common follow-up questions:
- Why is Set.size() = 2 for equal objects? — HashSet only checks equals inside a bucket; buckets are different → equals is never called
- How does the IDE help? — SonarQube (Critical Bug), SpotBugs, IntelliJ IDEA warn about it
- What is the Lombok pitfall? — @EqualsAndHashCode generates from ALL fields, including transient/cache
- Do Records solve the problem? — yes, they generate both methods from all components
Red flags (DO NOT say):
- “You can override only equals if objects aren’t in Map” — fragile decision
- “This is easy to notice” — no, it may manifest only under certain resize conditions
- “hashCode isn’t needed for identity semantics” — then don’t override equals either
Related topics:
- [[07. What is the equals() and hashCode() Contract]]
- [[08. If Two Objects Are Equal by equals(), What Can You Say About Their hashCode()]]
- [[11. What Happens If You Override hashCode() But Not equals()]]