Skip to content
QSWEQB
mediumConceptCodingEntryMidSenior

If you override equals but not hashCode, what breaks and when?

Equal objects must produce equal hash codes. Break that and a HashMap sends lookups to a bucket the entry was never placed in, so the map holds a value you can no longer retrieve and a set silently admits duplicates. The same failure appears later if a key is mutated after insertion.

4 min readUpdated 2026-07-26Asked at Infosys, TCS, Accenture, Wipro

What the interviewer is scoring

  • Does the candidate state both halves of the contract, not only the hash-code half
  • Whether the failure is described as a lookup miss in a specific bucket rather than vaguely as "the map misbehaves"
  • That they connect the contract to immutability without being led there
  • Can they explain why symmetry forces a choice between getClass and instanceof
  • Whether they know TreeMap ignores equals entirely and uses compareTo

Answer

Both halves of the contract

equals has four obligations that most candidates can recite: it is reflexive, symmetric, transitive, and consistent, and x.equals(null) is always false. The half that gets forgotten is the bridge to hashing, and it runs in one direction only. If two objects are equal then their hash codes must be equal. If two objects are unequal, their hash codes may collide freely — that is just a performance cost, not a correctness one.

The asymmetry matters because it tells you which mistake is fatal. Returning a constant from hashCode is legal and merely slow, since every entry piles into one bucket. Returning something derived from a field that equals ignores, or forgetting hashCode altogether so you inherit identity hashing from Object, is a correctness bug.

How the failure shows up

Hash-based lookup is two steps: compute the hash to pick a bucket, then walk that bucket comparing with equals. Overriding only equals means two objects that compare equal land in different buckets, so step one never brings them together and step two never gets to run. You call map.put(keyA, value), then map.get(keyB) with an equal key, and get null back while the entry sits in the table. A HashSet built from those objects reports a size larger than the number of distinct values, and contains returns false for a value the set is holding.

Nothing throws. That is what makes it a good interview question: the symptom surfaces far from the cause, usually as a duplicate row, a cache that never hits, or a deduplication step that quietly does nothing.

Writing the pair

import java.util.Objects;

public final class Money {
    private final String currency;
    private final long minorUnits;

    public Money(String currency, long minorUnits) {
        this.currency = Objects.requireNonNull(currency);
        this.minorUnits = minorUnits;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        // getClass() rather than instanceof: a subclass that adds state would
        // otherwise be equal to a Money instance while Money is not equal to it,
        // which breaks symmetry.
        if (o == null || getClass() != o.getClass()) return false;
        Money other = (Money) o;
        return minorUnits == other.minorUnits && currency.equals(other.currency);
    }

    @Override
    public int hashCode() {
        // Must be derived from exactly the fields equals compares, and no others.
        return Objects.hash(currency, minorUnits);
    }
}

The getClass versus instanceof choice is a real trade-off rather than a style preference. instanceof lets a subclass be equal to its superclass instance, which is what you want for something like a Hibernate proxy but which cannot be made symmetric if the subclass adds a field to the comparison. getClass keeps symmetry and costs you interoperability with proxies and generated subclasses. Declaring the class final, as above, removes the question.

Immutability is the same bug on a delay

A correct equals/hashCode pair still fails if the fields they read can change after the object is used as a key. The entry is placed in the bucket implied by the hash at insertion time and stays there. Mutate a field, and later lookups compute a different hash and search a different bucket, so the entry becomes unreachable through get, contains and remove while still being visible to iteration and still holding a reference to its value. That is a genuine leak, not just a lost lookup.

This is why the honest answer to "what makes a good map key" is: something immutable. Final class, final fields, defensive copies of anything mutable on the way in and on the way out, and no reference to this escaping the constructor. Immutability also buys you safe publication across threads, because the JVM guarantees that final fields written in a constructor are visible to any thread that observes a properly constructed reference.

Where strong candidates go further

The distinguishing move is to note that equals is not the only comparison Java uses. Sorted collections — TreeMap, TreeSet, and anything backed by a Comparator — never call equals at all; they decide identity by whether compareTo returns zero. So a class whose ordering is inconsistent with its equality behaves differently depending on which collection you drop it into. BigDecimal is the canonical example: 1.0 and 1.00 are not equals, so a HashSet keeps both, while compareTo returns zero, so a TreeSet keeps one. Neither collection is broken. The class simply has two notions of sameness, and picking a collection picks one of them.

The second move is to be precise about records. A record generates equals and hashCode from its components, which is correct and saves you the boilerplate above, but it delegates per component. A component whose type is an array gets Object's reference-based comparison, so two records holding arrays with identical contents are not equal. A component that is a mutable List makes the record's hash code mutable along with it.

Overriding one of equals and hashCode without the other is not a partial implementation, it is a broken one, and the collection will not tell you. The related discipline is that anything you use as a key should be immutable, because a mutated key produces exactly the same silent failure months after the code that broke it shipped.

Likely follow-ups

  • Why is a mutated key a memory leak and not just a failed lookup?
  • BigDecimal treats 1.0 and 1.00 as unequal under equals but equal under compareTo. What does that do to a HashSet versus a TreeSet?
  • How would you write equals for a JPA entity whose ID is database-generated?
  • Does a record's generated equals do the right thing for every component type?

Related questions

Further reading

equalshashcodeimmutabilityobject-semantics