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.
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.
| Method | Job | Used by | Must obey |
|---|---|---|---|
| equals() | Logical match | All objects | Reflexive, symmetric, transitive, consistent, non-null |
| hashCode() | Fast bucket hint | HashMap, HashSet | Same value for equal objects; stable while fields stay unchanged |
hashCode() on the key first. This is the cheap “which bucket?” step.(n - 1) & hash, where n is the table size. The table size is a power of two, so this is fast.equals() to find the exact key.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.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.
HashMap, elements in HashSet, or members of any hash-based cache, override both methods together.Object are fine: default equals() behaves like ==, and default hashCode() is identity-based for that JVM run.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.”
| Idea | Meaning | Default in Object | Typical use |
|---|---|---|---|
| Identity | Same instance | Yes | Low-level checks |
| Logical equality | Same value | No | Domain 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.
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:
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.equals() to separate them.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.equals() and hashCode() for their components automatically, which makes them a strong default choice for simple immutable data keys.HashMap call equals() before hashCode()? No. It computes the hash first to choose a bucket, then calls equals() only among candidates in that bucket.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.Tricky / gotcha questions:
equals() must still run.Common Mistakes:
equals() only. Correction: always override hashCode() at the same time, using the same fields.final when the object is stored in HashMap or HashSet.equals() is the final judge.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:
a.equals(b) is true, then a.hashCode() == b.hashCode() must be true.HashMap uses hash first, then equals().Object methods use identity, not business value.Practice Tasks:
equals() and hashCode().HashSet, then intentionally break one field after insertion and observe the failure.31 * hash implementation with Objects.hash(...) and note which one is clearer for your team.