What is the contract between hashCode() and equals()?
Equal objects must return the same hash code. Unequal objects are free to collide — that is legal and normal, not a bug. Break the first rule and hash-based collections lose your data silently.
The Answer
Say this in the room. 45 seconds.
- The contract is one-directional: if
a.equals(b), thena.hashCode() == b.hashCode(). - The converse is not required. Two unequal objects may share a hash code. That's a collision, and collisions are normal.
- So a
hashCode()that returns a constant is legal. It's slow, not wrong. hashCode()must also be consistent: the same object returns the same value while nothing about it changes.- Override one and you must override the other.
HashMap,HashSetandObjects.hashall assume it holds. - Break it and nothing throws. You just stop finding your own entries.
Understand It
The rule that only runs one way
Written out, the whole contract is three sentences:
| Rule | Consequence if you break it |
|---|---|
| Equal objects have equal hash codes | Hash collections silently lose entries |
| Unequal objects may have equal hash codes | Nothing — this is allowed and expected |
| The hash is stable while the object is unchanged | The entry becomes unreachable in place |
The middle row is the one people get wrong in interviews. They reach for "different objects must have different hash codes", which is not merely unrequired — it's impossible. hashCode() returns an int: about 4.3 billion values. There are more than 4.3 billion possible Strings. Collisions are guaranteed by counting, not by bad luck.
Two-character strings show it immediately:
System.out.println("\"Aa\".hashCode() = " + "Aa".hashCode());
System.out.println("\"BB\".hashCode() = " + "BB".hashCode());
System.out.println("but equal? " + "Aa".equals("BB"));"Aa".hashCode() = 2112
"BB".hashCode() = 2112
but equal? falseSame hash, not equal, and String is a JDK class whose hashCode() nobody would call broken. equals() is what decides identity. The hash only decides which bucket to look in.
Why a constant hash code is legal
If you accept that collisions are permitted, a hashCode() that returns 1 for everything is just the extreme case — every object collides with every other. It satisfies the contract, so the collection still behaves correctly:
Map<Const, String> m = new HashMap<>();
for (int i = 0; i < 1000; i++) m.put(new Const(i), "v" + i);
System.out.println("size = " + m.size());
System.out.println("get(new Const(500)) = " + m.get(new Const(500)));size = 1000
get(new Const(500)) = v500A thousand entries, all in one bucket, every lookup still correct. What you lost is speed, not correctness — that's O(log n) per lookup after the bin treeifies instead of O(1). Correctness and performance are separate axes here, and saying so is what distinguishes a real answer from a memorised one.
Breaking it in the direction that matters
Now the direction that does damage. Override equals() and leave hashCode() inherited from Object, which returns an identity-based value:
Map<Broken, String> m = new HashMap<>();
m.put(new Broken(1), "first");
System.out.println("equals says the same: " + new Broken(1).equals(new Broken(1)));
System.out.println("hashCodes match: "
+ (new Broken(1).hashCode() == new Broken(1).hashCode()));
System.out.println("get with an equal key: " + m.get(new Broken(1)));
System.out.println("containsKey: " + m.containsKey(new Broken(1)));
System.out.println("size: " + m.size());equals says the same: true
hashCodes match: false
get with an equal key: null
containsKey: false
size: 1The entry is in the map. size() counts it. Two objects that equals() calls identical, and the map finds neither from the other — because they hash to different buckets, so equals() is never even reached. No exception, no warning. This is the single most common cause of "my HashSet has duplicates in it".
What a correct pair looks like
Since Java 7 there is no reason to hand-roll either method:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order other)) return false;
return id == other.id && Objects.equals(region, other.region);
}
@Override
public int hashCode() {
return Objects.hash(id, region);
}
Three things to notice. instanceof with a pattern variable handles the null case for free — null instanceof Order is false, so no explicit null check. The two methods must read the same fields; that agreement is the contract, and it's why they belong next to each other in the file. And Objects.hash boxes its arguments into an array, so on a genuinely hot path a hand-written 31 * id + region.hashCode() still wins — measure before you care.
Records make the whole problem go away
A record generates both methods from its components, so they cannot drift:
System.out.println(new Point(1, 2).equals(new Point(1, 2)));
System.out.println(new Point(1, 2).hashCode() == new Point(1, 2).hashCode());true
trueAdd a field to a record and both methods update themselves. Add a field to a hand-written class and you have to remember two places. That's the real argument for records as key types, and it's a better answer than "less boilerplate".
The stability rule, and where it bites
The third rule — a stable hash while the object is unchanged — is the one people never quote and interviewers love. A mutable field that hashCode() reads is a live grenade: mutate it after insertion and the entry stays in the bucket chosen by the old hash, permanently unreachable.
Which is why key types should be immutable. Not "should ideally be" — the contract cannot be honoured by a mutable key that anyone actually mutates. That failure is the Challenge exercise on How does HashMap work internally?.
Interviewer's Next Move
The question after the question. This is what the round is actually testing.
1. "Must unequal objects have different hash codes?"
No. Only the forward direction is required. int has 4.3 billion values and String alone has more possible instances than that, so collisions are unavoidable by counting. "Aa" and "BB" both hash to 2112.
2. "What breaks if hashCode() returns a constant?" Nothing breaks. It's contract-legal and stays correct. Every key lands in one bucket, so lookups go O(n), then O(log n) once the bin treeifies at 8 nodes with a table of 64+. Correctness and performance are separate concerns.
3. "I overrode equals() and not hashCode(). What happens?"
Nothing at compile time and nothing at runtime — that's the danger. Equal objects hash to different buckets, so get() and containsKey() return nothing while size() still counts the entry. It's the usual cause of apparent duplicates in a HashSet.
4. "Does equals() have to be symmetric? What about across a subclass?"
Yes, and that's exactly where it breaks. If Sub extends Base adds a field and its equals() accepts a Base, you can get base.equals(sub) true while sub.equals(base) is false. instanceof is lenient and asymmetric; getClass() != o.getClass() is strict and symmetric but then no subclass instance ever equals a base instance. There is no answer that satisfies everyone, which is why record types are final.
5. "Why must a key be immutable?"
Because the third rule requires the hash to stay stable while the object is in the map. Mutate a hash-bearing field and the entry sits in the bucket for its old hash: unreachable by get(), unremovable by remove(), still counted by size(), still holding memory.
6. "Is Objects.hash() the right default?" Yes for readability, and it's what records generate. It allocates a varargs array and boxes primitives on every call, so on a proven hot path a hand-written accumulation avoids that. Profile before optimising.
Code traps
Trap A — predict before you run:
Set<StringBuilder> set = new HashSet<>();
StringBuilder sb = new StringBuilder("a");
set.add(sb);
sb.append("b");
System.out.println(set.contains(sb));
System.out.println(set.size());
Answer
true then 1. A surprise for the wrong reason: StringBuilder does not override hashCode() or equals(), so both are identity-based and mutation cannot change the hash. It works by accident. Swap StringBuilder for any class that hashes its own mutable content and the same code fails. Never conclude "mutable keys are fine" from this.
Trap B:
Map<Integer, String> m = new HashMap<>();
m.put(127, "a");
m.put(128, "b");
System.out.println(m.get(127) + " " + m.get(128));
Integer x = 127, y = 127, p = 128, q = 128;
System.out.println((x == y) + " " + (p == q));
Answer
a b then true false. The map is fine — it uses equals(), and Integer.equals compares values. The second line is the Integer cache: valueOf returns shared instances for −128..127, so == accidentally works below the boundary and fails above it. Proof that == on boxed types is a bug that hides in small test data.
Common wrong answers
| Said in interviews | Reality |
|---|---|
| "Different objects must have different hash codes." | Never required, and impossible for String. |
| "Equal hash codes mean the objects are equal." | Backwards. That's a collision; equals() decides. |
| "A constant hashCode() breaks the map." | Legal and correct. Only slow. |
| "hashCode() returns the memory address." | Object.hashCode() is identity-based, but it isn't the address and it isn't specified to be. |
| "If I override equals() the compiler warns me about hashCode()." | It does not. Some linters do; javac says nothing. |
Check Yourself
Q1. a.hashCode() == b.hashCode() is true. What, if anything, do you know about a.equals(b)?
Answer
Nothing at all. It's a collision or it's equality — only equals() can tell you. The implication runs the other way.
Q2. A class overrides hashCode() but not equals(). Can it be used as a HashMap key?
Answer
Yes, and it's safe — just not useful. equals() stays identity-based, so only the very same instance ever matches. No contract is violated: equal objects (identical ones) do have equal hashes. It's the reverse omission that loses data.
Q3. Why is record a better key type than a hand-written class with the same fields?
Answer
Both methods are generated from all components, so they cannot disagree and cannot go stale when a field is added. Records are also final, which sidesteps the subclass symmetry problem, and their components are final, which satisfies the stability rule.
Practice
| Tier | Exercise | Time |
|---|---|---|
| Warm-up | Collide two strings on purpose | 5 min |
| Challenge | The set that grew duplicates | 20 min |
| Production | The dedupe job that stopped deduping | 45 min |
| Interview | Full round replay | 10 min |
What changed, and when
Version boundaries are where follow-ups live. Know the change and the behaviour it replaced.
- Java 7
java.util.Objects arrives: Objects.hash(...) and Objects.equals(a, b) remove the null-checking boilerplate from both methods.
Before Java 7: Every equals() opened with a hand-rolled null and type check, and every hashCode() was a hand-written 31 * result + field loop. Both were copy-pasted, and both were where the bugs lived.
- Java 16
Records generate equals() and hashCode() from all components, so the two can no longer drift apart.
Before Java 16: You wrote them by hand or had the IDE generate them — and then added a sixth field and updated neither. Standard in 16; a preview feature in 14 and 15.
Practice ladder
Reading this page is not knowing it. Four tiers, ending in a real incident.
- Warm-up5 min
Collide two strings on purpose
One concept, guided. Near-impossible to fail.
- Challenge20 min
The set that grew duplicates
Edge cases. You have to reason, and two valid fixes differ.
- Production incident45 min
The dedupe job that stopped deduping
A real incident: symptom first, cause hidden, tradeoff at the end.
- Interview replay10 min
Full round replay — hashCode/equals
Timed verbal replay with pass/fail criteria per follow-up.
Where this question goes next
- How does HashMap work internally?
- mutable key problem — not written yet
- comparable vs comparator — not written yet
- record vs class — not written yet
Questions that lead here
How does HashMap work internally?
A lazily-created array of buckets indexed by a spread hash; collisions chain into a linked list that becomes a red-black tree only when the bin holds 8+ nodes AND the table is at least 64 slots.
Asked constantlyintermediate1–8 yrs9 min readCollectionsWhy is String immutable, and what is the string pool?
String is immutable because nothing in its API exposes a mutator — not because its byte[] field is final. The pool is a JVM-wide cache of literals that makes == accidentally work on literals and fail everywhere else.
Asked constantlyintermediate0–8 yrs9 min readStrings
Every runnable example above was compiled and executed against openjdk 21.0.11 on this build, and its output diffed against what this page claims. Last updated 2026-08-24.