Question 10 · Section 10

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.

Language versions: English Russian Ukrainian

🟢 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:

  • objA is 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) returns null, 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

  1. If an object will be a key in a hash collection — generate both methods. Exception: identity semantics is intentional.
  2. Use an IDE — automatic generation of equals() and hashCode()
  3. 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.
  4. 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()]]