skip to content

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%

answer

  1. Can't add value state to a subclass of an instantiable base and keep symmetry+transitivity+Liskov
  2. instanceof equality => asymmetry (base.equals(sub) true, sub.equals(base) false)
  3. getClass equality => symmetric but violates Liskov (sub never equals base)
  4. Resolution: composition over inheritance; make value types final/records
  5. Abstract base or no-new-equality subclass sidesteps it

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.

solid answer

~50 s

There's a fundamental tension: you cannot extend an instantiable class, add a value-significant field, and preserve the full equals contract while using instanceof-style equality. If the superclass uses instanceof and the subclass adds a field to equals(), then super.equals(sub) (ignoring the new field) can be true while sub.equals(super) is false — broken symmetry; mixing instances can also break transitivity. Using getClass() instead makes equality exact-class, which restores symmetry/transitivity but violates the Liskov substitution principle: a subclass instance can never be equal to a superclass instance, even when behaviorally substitutable, and breaks code that compares across the hierarchy (e.g. an unmodifiable-view wrapper). Effective Java's resolution is 'favor composition over inheritance': make the would-be subclass hold the base as a field and expose a view, sidestepping equality across types. Records and final classes avoid the problem by construction. hashCode must track whatever fields equals uses, so the same constraint flows through.

go deeper

for a junior

Likely just knows 'override both with the same fields'; this inheritance subtlety is beyond expected scope.

for a middle

Can recognize that adding a field in a subclass complicates equals, and that getClass vs instanceof differ, but may not fully articulate the symmetry/Liskov trade-off.

for a senior

Explains the instanceof-asymmetry vs getClass-Liskov dilemma, and recommends composition or final/record value types as the fix.

for a principal

States the impossibility result crisply, weighs Liskov vs symmetry consequences on real APIs (wrappers, proxies, unmodifiable views), and drives a codebase-wide convention (final value types / records / composition) with hashCode following mechanically.

## The setup `equals()` must be **symmetric** (`a.equals(b)` ⟺ `b.equals(a)`) and **transitive** (`a.equals(b)` and `b.equals(c)` ⟹ `a.equals(c)`). Separately, the interdependence rule forces `hashCode()` to use the same fields as `equals()`. Inheritance with added state stresses all of this. Take: ```java class Point { final int x, y; /* equals uses x,y */ } class ColorPoint extends Point { final Color color; /* wants color in equals */ } ``` ## Failure mode A — `instanceof` equality breaks symmetry If `Point.equals` uses `instanceof Point` and compares `x,y`, and `ColorPoint.equals` *also* requires matching `color`: ```java Point p = new Point(1, 2); ColorPoint cp = new ColorPoint(1, 2, RED); p.equals(cp); // true — Point.equals sees a Point, x/y match, ignores color cp.equals(p); // false — ColorPoint.equals demands color, p has none ``` `p.equals(cp) != cp.equals(p)` → **symmetry violated**. And with two ColorPoints of different colors both "equal" to the same Point, **transitivity** collapses too. Because `hashCode` must follow `equals`, you now also can't give these objects a consistent hash that satisfies the contract — hash-based collections misbehave. A tempting "blind" fix — make `ColorPoint.equals` ignore color when compared against a plain Point — restores symmetry but **destroys transitivity** (two differently-colored ColorPoints both equal the same Point but not each other) and is generally unsound. ## Failure mode B — `getClass()` equality breaks Liskov Switch every `equals` to `getClass() != o.getClass()` (exact-class equality). Now: ```java p.equals(cp); // false — different runtime classes cp.equals(p); // false — symmetric, good ``` Symmetry and transitivity are restored. **But** this violates the **Liskov Substitution Principle**: a `ColorPoint` can *never* be equal to a `Point`, even where the program treats a ColorPoint *as* a Point. This breaks real code — e.g. a `Collections.unmodifiableSet`-style or proxy *subclass* that adds no value-significant state is suddenly unequal to the base instance it wraps, even though it should compare equal. Exact-class equality also forbids legitimate "same logical value, helper subclass" equality. ## The principled resolutions 1. **Favor composition over inheritance (Effective Java, Item 10/18).** Don't make `ColorPoint extends Point`. Instead let it **hold** a `Point` and a `Color`, and expose `asPoint()` if a Point view is needed. Now `ColorPoint` and `Point` are unrelated types; there's no cross-type equality to get wrong, and each type's `equals`/`hashCode` are self-consistent. This is the recommended default. 2. **Make value classes `final` (or use `record`s).** If the class can't be subclassed, the whole dilemma disappears — there's no subclass to add state. Records enforce this (`record` is implicitly final and generates contract-correct `equals`/`hashCode` from components). This is why value types should be final. 3. **Add only equality-*neutral* state in subclasses.** A subclass may extend an instantiable class *without* overriding `equals`/`hashCode` — i.e. it adds no value-significant fields (perhaps just behavior). Then `instanceof` equality stays symmetric and Liskov-friendly. The rule is: *you may add state, OR you may add value-significant equality, but not both across an instantiable base.* 4. **Abstract base + concrete leaves.** If the base is **abstract** and can't be instantiated, concrete subclasses can each define equality over their own fields, and there's never a "plain base instance" to compare against, so the symmetry trap doesn't arise between a base and its subclass. ## hashCode in all this Whatever equality scheme you choose, `hashCode()` must be computed from exactly the fields `equals()` consults — including respecting the same instanceof-vs-getClass discipline. Get equals symmetric/transitive first; hashCode then follows mechanically (e.g. `Objects.hash(superFields..., ownFields...)`), and stays consistent only because it mirrors the equals field set. ## Takeaway There is **no** way to add value-significant state to a subclass of an instantiable class and keep all of: symmetry, transitivity, *and* Liskov substitutability. Accept the trade-off explicitly — and the cleanest escape is to not inherit: compose, or make the type final/`record`.

  • Why does the 'ignore the subclass field when comparing to a base instance' fix fail?
    It can restore symmetry but breaks transitivity: a RED ColorPoint and a BLUE ColorPoint can both be 'equal' to the same plain Point (color ignored), yet they are not equal to each other (colors differ). a~b and b~c but a≁c violates transitivity, which corrupts sets/maps in non-obvious ways.
  • How do records make this problem go away?
    A record is implicitly final, so it can't be subclassed to add equality-relevant state, and it auto-generates equals/hashCode from its components with exact field-set parity. With no subclass and contract-correct generated methods, neither the symmetry/transitivity trap nor the interdependence bug can occur.

saying these in an interview costs you the question

  • Claiming instanceof-based equals with an extra subclass field is symmetric
  • Asserting getClass-based equals satisfies Liskov substitutability
  • Thinking you can patch symmetry by 'ignoring' the extra field one-directionally (breaks transitivity)
  • Forgetting that hashCode must mirror whichever fields equals uses
  • Believing records can be subclassed to add equality state

context