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?
answer
- Can't add value state to a subclass of an instantiable base and keep symmetry+transitivity+Liskov
- instanceof equality => asymmetry (base.equals(sub) true, sub.equals(base) false)
- getClass equality => symmetric but violates Liskov (sub never equals base)
- Resolution: composition over inheritance; make value types final/records
- Abstract base or no-new-equality subclass sidesteps it
basics
~20 sIf 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 sThere'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
Likely just knows 'override both with the same fields'; this inheritance subtlety is beyond expected scope.
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.
Explains the instanceof-asymmetry vs getClass-Liskov dilemma, and recommends composition or final/record value types as the fix.
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