skip to content

equals/hashCode Interdependence

Override equals without hashCode and your object goes into a HashSet and can never be found again, because lookup starts from the bucket. This is the single most asked equals/hashCode question, usually framed as why my key disappeared.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

Why must you override hashCode() whenever you override equals() in Java?

level: juniorimportance: must knowfreq 90%

answer

  1. Two-step lookup: hashCode picks the bucket, equals confirms the entry
  2. Equal objects MUST share a hashCode (contract); unequal MAY collide
  3. Default hashCode is identity-based, so override-equals-only breaks it
  4. Wrong bucket => equals never called => object unretrievable
  5. Override both, from the SAME fields

basics

~20 s

Because Java's hash-based collections (like HashMap and HashSet) first use hashCode() to find the right bucket, then equals() to compare. If equal objects return different hash codes, they land in different buckets and the collection never finds the match.

solid answer

~40 s

There's a contract linking the two methods: if two objects are equal by equals(), they must return the same hashCode(). Override equals() alone and you break it, because the default Object.hashCode() returns a value tied to each object's identity, so two logically-equal objects get different hash codes. Hash-based collections (HashMap, HashSet) locate entries in two steps: compute hashCode() to pick a bucket, then walk that bucket calling equals(). If two equal objects hash differently, they go to different buckets, so a lookup that hashes one will never even visit the bucket holding the other, and equals() is never consulted. The result: contains() returns false for an object that is equal to a stored one, and a HashSet can hold two equal elements. Always override both together, using the same fields in each.

go deeper

for a junior

Knows you must override both together and that HashMap/HashSet misbehave otherwise. Can state the rule even without explaining bucket internals.

for a middle

Can explain the two-step hash-then-equals lookup and why a wrong hashCode sends the lookup to the wrong bucket so equals is never called.

for a senior

Articulates the full contract (equal => same hash; unequal MAY collide), the default identity-hash behavior, and demonstrates a correct paired implementation using Objects.hash with matching fields.

for a principal

Discusses downstream consequences (silent data-integrity bugs, mutable-key hazards, distribution quality of hash functions, IDE/record/Lombok generation as the standard mitigation) and sets team conventions to prevent the bug.

## The two methods and what they mean Every Java object inherits two methods from `java.lang.Object`: - **`equals(Object o)`** — answers "is this object *logically the same* as that one?" The default implementation is **reference identity**: `a.equals(b)` is true only when `a` and `b` are literally the *same* object in memory (the same as `a == b`). You override it when two *separate* objects should count as equal — e.g. two `Point(1,2)` instances. - **`hashCode()`** — returns an `int` that acts as a fast, lossy "summary" of the object. The default returns a number derived from the object's identity (historically its memory address), so two distinct objects almost always get *different* hash codes. ## The contract that binds them The Javadoc of `Object` states a **contract**: > If two objects are equal according to `equals()`, then calling `hashCode()` on each **must** produce the **same** integer. (The reverse is NOT required: two *unequal* objects MAY share a hash code — that's a *collision*, and it's allowed.) When you override `equals()` to compare by fields but leave `hashCode()` inherited, you break this contract: two objects that are now `equals()`-equal can still return the two *different* identity-based hash codes they inherited. The compiler does not catch this — it's a silent logic bug. ## Why hash-based collections care A **hash table** (the engine inside `HashMap` and `HashSet`) stores entries in an array of *buckets*. To store or find a key it does **two steps**: 1. **Hash step:** call `key.hashCode()`, reduce it (e.g. modulo the array length) to pick **one bucket**. 2. **Equality step:** scan only the entries *in that bucket*, calling `equals()` on each, to find the exact match. This two-step design is what makes lookups average O(1) instead of O(n): you never scan the whole table, only one small bucket. Now suppose you stored `new Point(1,2)` and later call `set.contains(new Point(1,2))`. The two Points are `equals()`-equal but, with the broken setup, have **different** hash codes. Step 1 sends the lookup to a *different bucket* than where the stored Point lives. Step 2 scans that (wrong, probably empty) bucket, never finds the entry, and `equals()` is **never even called** on the stored Point. So: - `contains()` / `get()` return **false / null** for an object that *is* equal to a stored one — the object is effectively **unretrievable**. - `HashSet` can end up holding **two objects that are equal**, because `add()` uses the same broken lookup to decide "is this a duplicate?" and concludes it isn't. ## The fix Override **both** methods, **using the same set of fields** in each. If `equals()` compares `x` and `y`, then `hashCode()` must be computed from `x` and `y` too (e.g. `return Objects.hash(x, y);`). Then equal objects always hash identically, the lookup reaches the right bucket, and the collection behaves correctly. ## Key intuition to keep `hashCode()` decides *where to look*; `equals()` decides *which one it is*. Get the "where" wrong and the "which" never runs.

  • If two objects have the same hashCode, does that guarantee they are equal?
    No. Equal objects must share a hash code, but the reverse is not required — different objects may collide on the same hash code. The collection resolves collisions by then calling equals() within the bucket.
  • Does overriding only hashCode() (and leaving equals() default) cause the same problem?
    It doesn't violate the contract the same way, but it's pointless: equals() still uses identity, so logically-equal objects are still treated as different. You'd never get duplicate-detection or retrieval to work. The contract is only broken when equals says 'equal' while hashCode disagrees.

saying these in an interview costs you the question

  • Saying 'hashCode and equals are unrelated, override whichever you need'
  • Claiming unequal objects must have different hash codes (collisions are allowed)
  • Thinking the compiler or runtime warns you about the broken contract
  • Believing a List or array lookup is affected (only hash-based collections are)
  • Including different fields in hashCode than in equals

context

open as a page

Walk through exactly how HashMap uses hashCode() and equals() during a get(), and pinpoint where a broken hashCode causes the lookup to fail.

level: middleimportance: must knowfreq 75%

basics

~20 s

HashMap first calls hashCode() on the key to choose a bucket (an array slot), then walks the entries in that bucket calling equals() to find the exact key. If hashCode() is wrong, it picks the wrong bucket, so the right entry is never visited and equals() never runs.

open as a page

Show a correct, contract-honoring equals()/hashCode() pair and explain how each line keeps the two consistent.

level: middleimportance: should knowfreq 70%

basics

~20 s

Compare the same fields in both methods. In equals(): check identity, check the type, then compare each field (null-safe with Objects.equals). In hashCode(): combine those same fields with Objects.hash(...). Same fields in => consistent results out.

open as a page

What happens if you mutate a field used in hashCode() after an object is already a key in a HashMap, and how do you avoid it?

level: seniorimportance: should knowfreq 55%

basics

~20 s

The object was filed in a bucket based on its old hash. After mutation its hash changes, so a fresh lookup computes the new hash and searches a different bucket. The entry is still in the old bucket, so the map can't find it — the key becomes a 'lost' entry that you can't get or remove normally.

open as a page

How can a naive equals()/hashCode() pair in a subclass that adds a field violate the contract or the equals symmetry/transitivity rules, and what are the principled options?

level: principalimportance: nice to knowfreq 35%

basics

~20 s

If a subclass adds a field and includes it in equals()/hashCode() while a superclass instance ignores it, comparisons can become asymmetric (a.equals(b) but not b.equals(a)) or break transitivity. The safe options are: don't add equality-relevant state to subclasses, use composition instead of inheritance, or use exact-class (getClass) equality.

open as a page