RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
TrickyJava#427 min readJul 11, 2026

Contract between equals() and hashCode().

java
hashmap
practice
object
learning
Practice modeTest yourself instead of reading straight through

Hook: Interviewers love this question because one tiny mistake can make a HashMap silently stop finding the right object.

Question: Contract between equals() and hashCode().

Answer: If two objects are equal according to equals(), they must return the same hashCode(). The reverse is not required: two different objects may share a hash code, which is called a collision. This matters because hash-based collections like HashMap and HashSet use the hash code to find a bucket first, then use equals() to confirm the exact match.

Interview-Ready Answer: The contract is: whenever a.equals(b) is true, a.hashCode() == b.hashCode() must also be true. But the reverse is not guaranteed, so equal hash codes do not mean equal objects. I think of equals() as logical identity and hashCode() as the fast index used by hash tables. If I break that contract, HashMap and HashSet can miss keys that are actually equal in business terms.

🧠 Memory Map
Memory map — visual summary of this topic

What the contract really says

Detailed Explanation: Java has two related ideas here. equals() answers, “Are these two objects logically the same thing?” hashCode() returns an int used to group objects quickly. The official rule is one-way: equal objects must have the same hash code. Unequal objects may still collide, and that is normal.

MethodJobUsed byMust obey
equals()Logical matchAll objectsReflexive, symmetric, transitive, consistent, non-null
hashCode()Fast bucket hintHashMap, HashSetSame value for equal objects; stable while fields stay unchanged

How hash-based collections use it under the hood

  1. Java calls hashCode() on the key first. This is the cheap “which bucket?” step.
  2. HashMap spreads the bits and computes an index, roughly (n - 1) & hash, where n is the table size. The table size is a power of two, so this is fast.
  3. If several keys land in the same bucket, Java walks that bucket and calls equals() to find the exact key.
  4. If a bucket gets crowded, modern HashMap (Java 8+) can turn a long list into a balanced tree. That makes heavy-collision cases faster, moving from about O(n) toward O(log n) for that bucket.
  5. When the map grows past its load factor, the table resizes; the default initial capacity is 16 and the default load factor is 0.75. Keys are re-bucketed using their hash codes, so a bad or unstable hash breaks lookups.

The key mental model is: hashCode() narrows the search, equals() finishes it. A collision is not an error; it just means two different objects share the same bucket. What is an error is saying two objects are equal but giving them different hashes, because then they may go to different buckets and never be compared with equals() at all.

When and why to override both

  • If your objects are used as keys in HashMap, elements in HashSet, or members of any hash-based cache, override both methods together.
  • If you use only identity meaning “same Java instance,” the defaults from Object are fine: default equals() behaves like ==, and default hashCode() is identity-based for that JVM run.
  • Prefer immutable fields for keys. A field is immutable when it does not change after construction. If a field used in equals() or hashCode() changes after insertion, the object can become impossible to find or remove.
  • Objects.hash(...) is convenient and readable, but it creates some overhead because it uses a varargs array. For hot paths or very simple classes, a manual 31 * result + fieldHash style is often faster.

Comparison: Identity and logical equality are not the same thing. Identity says “same instance”; logical equality says “same business value.”

IdeaMeaningDefault in ObjectTypical use
IdentitySame instanceYesLow-level checks
Logical equalitySame valueNoDomain objects

Important edge cases: Two different objects can have the same hash code, and that is fine. But if you override equals() and forget hashCode(), a HashSet may store duplicates and a HashMap may fail to find an equal key. Also, be careful with subclasses: inheritance can easily break symmetry if one class compares extra fields that the parent does not know about. Using final classes or a careful canEqual pattern avoids that trap.

Performance note: The average cost of HashMap lookup is O(1) when hashes are good. In the worst case, poor hashes or massive collisions can push it toward O(n), or O(log n) in treeified bins on modern JDKs. So a good hashCode() is not just correctness; it is also speed.

Real-world story: Imagine a checkout service that stores applied discount keys in a HashSet so the same coupon cannot be used twice. A developer overrides equals() on CouponKey but forgets hashCode(). In testing, two coupon objects print the same business fields, yet the set still accepts both because they land in different buckets. In production, users see duplicate discounts, support sees inconsistent totals, and logs show repeated “coupon not found in set” messages even though the coupon data is clearly present.

The scary part is that the bug does not crash the app; it quietly corrupts business logic. If the key is also mutable, an update after insertion can make the entry impossible to remove, leaving stale data in memory. Symptoms are subtle: cache-hit rate drops, containsKey() returns false for an object that was just inserted, and debugging shows the map holding an entry that cannot be reached by normal lookup.

Java
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        // Immutable keys are safe: the fields used in equals/hashCode never change.
        CustomerKey k1 = new CustomerKey("C-100", 1);
        CustomerKey k2 = new CustomerKey("C-100", 1);
        CustomerKey k3 = new CustomerKey("C-100", 2);

        System.out.println("k1.equals(k2) = " + k1.equals(k2));
        System.out.println("k1.hashCode() == k2.hashCode() = " + (k1.hashCode() == k2.hashCode()));
        System.out.println("k1.equals(k3) = " + k1.equals(k3));
        System.out.println("k1.hashCode() == k3.hashCode() = " + (k1.hashCode() == k3.hashCode()));

        Map<CustomerKey, String> cache = new HashMap<>();
        cache.put(k1, "cached-value");

        // Because k2 is equal to k1, HashMap can find the entry.
        System.out.println("cache.get(k2) = " + cache.get(k2));
        System.out.println("cache.get(k3) = " + cache.get(k3)); // different value, so no match

        // Edge case: mutable keys break hash-based collections.
        MutableOrderKey mutable = new MutableOrderKey("O-77", 1);
        Map<MutableOrderKey, String> orders = new HashMap<>();
        orders.put(mutable, "stored");

        System.out.println("before mutation, containsKey(mutable) = " + orders.containsKey(mutable));
        System.out.println("before mutation, get(mutable) = " + orders.get(mutable));

        // Changing a field used by hashCode() changes the object's bucket identity.
        mutable.setLineNo(2);

        // The entry is still in the map, but lookups may fail because the hash no longer matches.
        System.out.println("after mutation, containsKey(mutable) = " + orders.containsKey(mutable));
        System.out.println("after mutation, get(mutable) = " + orders.get(mutable));
        System.out.println("map still holds entry: " + orders.entrySet());
    }
}

final class CustomerKey {
    private final String customerId;
    private final int regionId;

    CustomerKey(String customerId, int regionId) {
        this.customerId = customerId;
        this.regionId = regionId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof CustomerKey)) return false;
        CustomerKey that = (CustomerKey) o;
        return regionId == that.regionId && Objects.equals(customerId, that.customerId);
    }

    @Override
    public int hashCode() {
        int result = (customerId != null ? customerId.hashCode() : 0);
        result = 31 * result + regionId;
        return result;
    }

    @Override
    public String toString() {
        return "CustomerKey{" + "customerId='" + customerId + '\'' + ", regionId=" + regionId + '}';
    }
}

class MutableOrderKey {
    private String orderId;
    private int lineNo;

    MutableOrderKey(String orderId, int lineNo) {
        this.orderId = orderId;
        this.lineNo = lineNo;
    }

    void setLineNo(int lineNo) {
        this.lineNo = lineNo;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof MutableOrderKey)) return false;
        MutableOrderKey that = (MutableOrderKey) o;
        return lineNo == that.lineNo && Objects.equals(orderId, that.orderId);
    }

    @Override
    public int hashCode() {
        int result = (orderId != null ? orderId.hashCode() : 0);
        result = 31 * result + lineNo;
        return result;
    }

    @Override
    public String toString() {
        return "MutableOrderKey{" + "orderId='" + orderId + '\'' + ", lineNo=" + lineNo + '}';
    }
}

Follow-up & Tricky Questions:

  • Why do we override both methods together? Because hash-based collections use hashCode() to find a bucket and equals() to verify the exact object. If only one is overridden, the collection’s view of equality becomes inconsistent.
  • Can two unequal objects have the same hash code? Yes. That is normal and called a collision; the collection then uses equals() to separate them.
  • What happens if equals() is overridden but hashCode() is not? Equal objects may land in different buckets, so HashMap.get() and HashSet.contains() can fail even when the objects look equal.
  • Why should key fields be immutable? If a field changes after insertion, the object’s hash can change, and the collection may no longer be able to find or remove that entry.
  • How do records help here? Java records generate value-based equals() and hashCode() for their components automatically, which makes them a strong default choice for simple immutable data keys.
  • Does HashMap call equals() before hashCode()? No. It computes the hash first to choose a bucket, then calls equals() only among candidates in that bucket.
  • Can hashCode() return the same number for all objects? Technically yes, but it would make hash tables degenerate into long scans, so the code would be correct but very slow.
  • If two objects have different hash codes, can they still be equal? No. That would violate the contract, because equal objects must always have the same hash code.

Tricky / gotcha questions:

  • Is a unique hash code required? No. Hash codes are not IDs; they are just fast grouping values, so collisions are expected.
  • Does a matching hash code prove equality? No. It only proves the objects might be equal, so equals() must still run.
  • What breaks first: performance or correctness? Both can break, but correctness is the bigger danger: lookups may fail, duplicates may appear, and removals may miss the actual entry.

Common Mistakes:

  • Overriding equals() only. Correction: always override hashCode() at the same time, using the same fields.
  • Using mutable fields in a key. Correction: make key fields final when the object is stored in HashMap or HashSet.
  • Thinking same hash means same object. Correction: collisions are allowed; equals() is the final judge.
  • Forgetting the equals() contract itself. Correction: keep it reflexive, symmetric, transitive, consistent, and non-null.

Memory Hook: Think of equals() as the badge at the door and hashCode() as the locker number. The locker gets you to the right hallway fast, but the badge still decides whether the person is really the one you want.

Cheat Sheet:

  • If a.equals(b) is true, then a.hashCode() == b.hashCode() must be true.
  • The reverse is not required; collisions are normal.
  • HashMap uses hash first, then equals().
  • Use immutable fields for keys whenever possible.
  • Java 8+ can treeify crowded buckets, but good hashes are still the main defense.
  • Default Object methods use identity, not business value.

Practice Tasks:

  • Write a small class with two fields and implement correct equals() and hashCode().
  • Put that class into a HashSet, then intentionally break one field after insertion and observe the failure.
  • Compare a manual 31 * hash implementation with Objects.hash(...) and note which one is clearer for your team.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

import java.util.HashMap; import java.util.Map; import java.util.Objects; public class Main { public static void main(String[] args) { // Immutable keys are safe: the fields used in equals/hashCode never change. CustomerKey k1 = new CustomerKey("C-100", 1); CustomerKey k2 = new CustomerKey("C-100", 1); CustomerKey k3 = new CustomerKey("C-100", 2); System.out.println("k1.equals(k2) = " + k1.equals(k2)); System.out.println("k1.hashCode() == k2.hashCode() = " + (k1.hashCode() == k2.hashCode())); System.out.println("k1.equals(k3) = " + k1.equals(k3)); System.out.println("k1.hashCode() == k3.hashCode() = " + (k1.hashCode() == k3.hashCode())); Map<CustomerKey, String> cache = new HashMap<>(); cache.put(k1, "cached-value"); // Because k2 is equal to k1, HashMap can find the entry. System.out.println("cache.get(k2) = " + cache.get(k2)); System.out.println("cache.get(k3) = " + cache.get(k3)); // different value, so no match // Edge case: mutable keys break hash-based collections. MutableOrderKey mutable = new MutableOrderKey("O-77", 1); Map<MutableOrderKey, String> orders = new HashMap<>(); orders.put(mutable, "stored"); System.out.println("before mutation, containsKey(mutable) = " + orders.containsKey(mutable)); System.out.println("before mutation, get(mutable) = " + orders.get(mutable)); // Changing a field used by hashCode() changes the object's bucket identity. mutable.setLineNo(2); // The entry is still in the map, but lookups may fail because the hash no longer matches. System.out.println("after mutation, containsKey(mutable) = " + orders.containsKey(mutable)); System.out.println("after mutation, get(mutable) = " + orders.get(mutable)); System.out.println("map still holds entry: " + orders.entrySet()); } } final class CustomerKey { private final String customerId; private final int regionId; CustomerKey(String customerId, int regionId) { this.customerId = customerId; this.regionId = regionId; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof CustomerKey)) return false; CustomerKey that = (CustomerKey) o; return regionId == that.regionId && Objects.equals(customerId, that.customerId); } @Override public int hashCode() { int result = (customerId != null ? customerId.hashCode() : 0); result = 31 * result + regionId; return result; } @Override public String toString() { return "CustomerKey{" + "customerId='" + customerId + '\'' + ", regionId=" + regionId + '}'; } } class MutableOrderKey { private String orderId; private int lineNo; MutableOrderKey(String orderId, int lineNo) { this.orderId = orderId; this.lineNo = lineNo; } void setLineNo(int lineNo) { this.lineNo = lineNo; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof MutableOrderKey)) return false; MutableOrderKey that = (MutableOrderKey) o; return lineNo == that.lineNo && Objects.equals(orderId, that.orderId); } @Override public int hashCode() { int result = (orderId != null ? orderId.hashCode() : 0); result = 31 * result + lineNo; return result; } @Override public String toString() { return "MutableOrderKey{" + "orderId='" + orderId + '\'' + ", lineNo=" + lineNo + '}'; } }