skip to content

When equals/hashCode are inherited across a class hierarchy, how can a subclass adding a field break the hashCode contract, and how do you avoid it?

level: principalimportance: nice to knowfreq 28%

answer

  1. Add a value field via subclass -> symmetry or LSP breaks
  2. instanceof breaks symmetry; getClass breaks Liskov
  3. No perfect inheritance solution for value classes
  4. Fix: composition over inheritance (hold a Point field)
  5. Records/final value classes dodge it entirely

basics

~20 s

If a subclass adds a field and changes equals/hashCode to use it, a parent object and a child object can end up 'equal' one way but with different hashCodes, breaking the rule that equal objects share a code. Avoid it by using composition instead of extending value classes, or by comparing exact classes.

solid answer

~50 s

The hazard appears when an instantiable parent class defines value-based equals/hashCode and a subclass adds a significant field. If the subclass's equals uses `instanceof` against the parent type, you can get asymmetry: a parent considers a child equal (it only checks parent fields) while the child considers the parent unequal (the extra field differs) — and their hashCodes differ, violating the equal-objects-share-a-code rule once the relation is reconciled. Using `getClass()` instead of `instanceof` restores symmetry but breaks Liskov substitution: a subclass instance is never equal to a parent instance even when it should be substitutable. There is no fully satisfying way to add a value field to an instantiable class via inheritance. The standard resolution from Effective Java is **favor composition over inheritance**: give the would-be subclass a private field of the parent type and a view accessor, instead of extending. Alternatively keep the value class effectively final, or restrict equality/hashCode to an immutable, abstract-class-rooted hierarchy where no instantiable level adds equals-relevant state.

code

java · 15 lines
java
// PROBLEM: extending an instantiable value class
class Point {
    final int x, y;
    Point(int x, int y){ this.x=x; this.y=y; }
    @Override public boolean equals(Object o){
        return o instanceof Point p && p.x==x && p.y==y;
    }
    @Override public int hashCode(){ return java.util.Objects.hash(x,y); }
}
// colorPoint.equals(point) vs point.equals(colorPoint) -> asymmetric, codes differ

// FIX: composition — ColorPoint HAS-A Point, no inheritance
record ColorPoint(Point point, String color) {   // record: final + correct equals/hashCode
    Point asPoint(){ return point; }
}

go deeper

for a junior

Aware that subclassing a class with custom equals/hashCode can cause surprising bugs and that records/final classes are safer for keys.

for a middle

Can show the asymmetry from instanceof-based equals and knows that equal objects must share a hashCode, so the break propagates to hashCode too.

for a senior

Explains the instanceof-vs-getClass trade-off (symmetry vs Liskov), and applies composition-over-inheritance or final/record value classes as the fix.

for a principal

Frames it as an inherent limitation of inheritance for value types, sets team policy (records/final keys, getClass only for sealed/final), and connects the failure to concrete HashSet membership anomalies in production.

## The core tension `equals` and `hashCode` are *paired*: equal objects must share a hashCode. Inheritance complicates this whenever an **instantiable** class has value-based equals/hashCode and a subclass introduces a new field that *should* count toward equality. You then must decide how a parent instance and a child instance relate — and every choice has a flaw. ## Setup ``` class Point { int x, y; // value-based equals/hashCode on (x, y) } class ColorPoint extends Point { Color c; // wants equality on (x, y, c) } ``` ### Option A: subclass equals uses `instanceof Point` and checks only x,y when comparing to a plain Point This tries to keep `Point` and `ColorPoint` comparable, but breaks **symmetry**: `point.equals(colorPoint)` may be `true` (Point checks only x,y) while `colorPoint.equals(point)` is `false` (ColorPoint also wants the color to match, but a Point has none). `equals` *must* be symmetric, so this is already a contract violation — and because the two objects are 'equal' in one direction yet have **different hashCodes** (ColorPoint folds `c` in), the equal-implies-equal-hash rule is also jeopardized. Worse, mixing such objects in a `HashSet` produces inconsistent, order-dependent membership results. ### Option B: every equals uses `getClass()` (exact-class check) Now `point.equals(colorPoint)` is `false` both ways (different classes) — symmetry and the hashCode link are restored. **But** this violates the **Liskov Substitution Principle**: a `ColorPoint` can no longer stand in for a `Point` in equality-sensitive code, even though it *is* a Point. If some library treats all Points equal by location (e.g. a set of occupied positions), a ColorPoint will never be recognized as occupying a Point's spot. So you've traded a contract violation for a substitutability violation. ### The deeper truth *There is no way to extend an instantiable class with a new value-significant field while preserving the equals/hashCode contract perfectly.* This is a fundamental limitation, not a coding mistake. ## The recommended resolutions 1. **Favor composition over inheritance (Effective Java).** Don't make `ColorPoint extends Point`. Instead, give `ColorPoint` a *private* `Point` field plus the color, and expose `asPoint()` if a Point view is needed. Now `ColorPoint`'s equals/hashCode are over `(point, color)` as a self-contained value type; there's no parent/child equality to reconcile, so no asymmetry and no LSP problem. 2. **Make value classes effectively final.** If a class has value-based equals/hashCode, prevent subclassing (mark it `final`, or use a `record` — records are implicitly final and generate compliant equals/hashCode from their components). This removes the hazard at the source. 3. **Abstract-root hierarchies with no instantiable equals-adding level.** It *is* acceptable for an **abstract** superclass to define equals/hashCode that concrete subclasses extend, *provided* you cannot create instances of an intermediate instantiable level that adds value fields — because then there's no parent *object* to compare against asymmetrically. ## Practical guidance for a tech lead - Default to `record`s or `final` value classes for anything used as a map/set key. - Reserve `getClass()`-based equals for genuinely final or sealed value types where substitutability of subtypes is intentionally disallowed. - If you must keep an open class comparable, document that subclasses **must not** add equals-relevant state, and enforce via review/architecture tests. - Remember the symptom in production: a value-typed item appears to be 'in' a `HashSet` from one reference and 'not in' from another, or duplicates slip into a set — a tell-tale of broken symmetry/hash linkage across a hierarchy.

  • Why doesn't switching to getClass() in equals fully solve the problem?
    getClass() restores symmetry and the hashCode link, but it makes a subclass instance never equal to a parent instance, violating the Liskov Substitution Principle — a subtype that should be usable wherever the supertype is expected is now treated as a different value. It trades one defect for another.
  • Do Java records make this go away?
    Largely, yes. Records are implicitly final, so you cannot subclass them to add value fields, and their generated equals/hashCode are derived consistently from the components. Combined with composition (a record holding another record), you avoid the hierarchy-equality reconciliation entirely.

It's like trying to make a passport (name) and a passport-with-visa (name + visa) treat each other as the same traveler. Border A ignores the visa and waves both through (symmetric only if everyone ignores it); border B demands an exact document match and rejects the upgraded passport as 'a different document.' You can't satisfy both — so you stop subtyping and just bundle the visa alongside a copy of the passport instead.

saying these in an interview costs you the question

  • Claiming you can cleanly add a value field via subclassing without breaking the contract
  • Using instanceof equals across a hierarchy without realizing it breaks symmetry
  • Asserting getClass() equals is universally correct (it breaks Liskov)
  • Mixing parent and value-extending child instances in the same HashSet
  • Forgetting that the hashCode link breaks alongside the symmetry break

context