Inheritance and equals() Symmetry

Equality designed across an inheritance hierarchy can violate the `equals()` contract even when each class's implementation looks reasonable in isolation. The main danger is allowing a superclass and subclass to use different criteria for deciding whether two objects are equal.

Java's `equals()` contract requires symmetry: if `a.equals(b)` is true, then `b.equals(a)` must also be true. In an inheritance hierarchy, this can break when a superclass uses `instanceof` while a subclass adds fields and uses stricter type or state checks.

For example, suppose `Money.equals()` considers any `Money` instance with the same amount equal, while `Voucher.equals()` additionally requires the same store. A `Money` reference can consider a `Voucher` equal because the voucher is a `Money`, while the voucher can reject the `Money` because it lacks the required voucher state. The result is `money.equals(voucher) != voucher.equals(money)`.

Transitivity can also fail when equality rules differ across multiple subclasses. If a base class treats several subclasses as comparable but subclasses introduce additional equality criteria, a chain of objects can satisfy `a.equals(b)` and `b.equals(c)` while `a.equals(c)` is false.

A common defensive strategy is to make equality type-strict with `getClass()` when a class is intended to have equality based on its complete state. Another option is to make the base class equality final, or design the hierarchy so subclasses do not add equality-significant state. The right choice depends on the intended equality semantics, but it must preserve the `equals()` contract.

`hashCode()` must follow the same equality definition: whenever two objects are equal, their hash codes must match. Changing equality semantics in a subclass without carefully considering hashing can therefore create incorrect behavior in `HashMap` and `HashSet`.

Key points

Interview questions

Why can inheritance cause an `equals()` symmetry violation?

A superclass may use `instanceof` and compare only its own fields, thereby accepting a subclass as equal. The subclass may compare its additional state and reject the superclass instance. Consequently, `superObject.equals(subObject)` can differ from `subObject.equals(superObject)`, violating symmetry.

When would you prefer `getClass()` over `instanceof` in `equals()`?

`getClass()` is appropriate when equality is defined by the complete state of one concrete class and subclasses should not be considered equal to instances of that class. It prevents cross-class comparisons from silently entering the equality relation. This is particularly useful for value-like classes where subclasses add state that matters to equality.

What is the relationship between `equals()` and `hashCode()`?

If two objects are equal according to `equals()`, they must return the same `hashCode()`. The reverse is not required: different objects may have the same hash code because collisions are allowed. Violating the equal-implies-same-hash-code rule can make hash-based collections behave incorrectly.

How can you design a subclass hierarchy safely when subclasses need additional equality state?

First define whether superclass and subclass instances are genuinely interchangeable for equality. If subclass state changes the value's identity, allowing the superclass to compare arbitrary subclasses is usually unsafe. Options include exact-class equality, making the superclass equality implementation final, avoiding equality-significant subclass state, or replacing inheritance with composition.

Besides symmetry, which parts of the `equals()` contract are especially relevant to inheritance?

Reflexivity, symmetry, transitivity, consistency, and the rule that comparison with `null` returns false are the key requirements. Inheritance most commonly creates problems with symmetry and transitivity because different classes can apply different equality criteria. The implementation should also remain consistent with `hashCode()`.