Symmetric equals trap with inheritance — show with code. — Cracked Java
// Java Collections Framework · equals and hashCode Contract
SeniorTrickBig TechGoogle

Symmetric equals trap with inheritance — show with code.

Adding a value component in a subclass and using instanceof in equals breaks symmetry. This is the classic Point/ColorPoint trap from Effective Java (Item 10), and it's the reason value classes should be final or use composition.

The setup

public 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 Objects.hash(x, y); }
}

public class ColorPoint extends Point {
    final Color color;
    ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    // Naive override: also check color
    @Override
    public boolean equals(Object o) {
        return o instanceof ColorPoint cp
            && super.equals(cp)
            && cp.color == color;
    }
}

The trap

Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);

p.equals(cp);   // true  — Point.equals checks (x,y) only, sees instanceof Point pass
cp.equals(p);   // false — ColorPoint.equals checks instanceof ColorPoint, p is just a Point

Symmetry is broken. Now HashSet.contains is non-deterministic: depending on which object is the receiver, you get different answers.

"Fix" attempt that creates a new bug

Use getClass() instead of instanceof:

@Override
public boolean equals(Object o) {
    return o != null && o.getClass() == getClass()
        && ((Point) o).x == x && ((Point) o).y == y;
}

Now p.equals(cp) is false (different classes), and so is cp.equals(p) — symmetric again. But this violates the Liskov Substitution Principle: a ColorPoint can no longer be used wherever a Point is expected for equality purposes. If a method takes a Set<Point> and you pass a set built from ColorPoints, lookups with plain Point keys silently fail.

What to do instead

  1. Make value classes final so they can't be subclassed.
  2. Use composition — ColorPoint has a Point, doesn't extend one. (See the next question.)
  3. Use records — records are implicitly final and auto-generate a correct equals.
public record ColorPoint(Point location, Color color) {} // safe, final, symmetric

Mark your status