equals Across a Class Hierarchy

OOP · lesson 36 of 43 · 4 min read

Why adding a field in a subclass breaks equals symmetry, and the two honest ways out of it.

Open this lesson in the learning hub

Key points

  • equals must be symmetric: if a.equals(b) is true then b.equals(a) must be true as well.
  • Add a field in a subclass and an instanceof based equals loses that: the parent says yes, the child says no.
  • Then contains answers differently depending on which object you search with, because it calls equals on the argument.
  • Switching to getClass() != o.getClass() restores symmetry, but no subclass instance can ever equal a parent one.
  • No version keeps both. Make the class final, or hold the value in a field instead of extending it.
  • Records dodge the problem completely: they are final, so this kind of hierarchy cannot exist at all.

Example

import java.util.List;

public class Main {

    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }

        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;   // any Point will do
            return x == p.x && y == p.y;
        }

        @Override public int hashCode() { return 31 * x + y; }

        @Override public String toString() { return "Point(" + x + "," + y + ")"; }
    }

    static final class ColourPoint extends Point {
        final String colour;

        ColourPoint(int x, int y, String colour) { super(x, y); this.colour = colour; }

        @Override public boolean equals(Object o) {
            if (!(o instanceof ColourPoint c)) return false;   // stricter than the parent
            return super.equals(o) && colour.equals(c.colour);
        }

        @Override public int hashCode() { return 31 * super.hashCode() + colour.hashCode(); }

        @Override public String toString() { return "ColourPoint(" + x + "," + y + "," + colour + ")"; }
    }

    public static void main(String[] args) {
        Point p = new Point(1, 2);
        ColourPoint cp = new ColourPoint(1, 2, "red");

        System.out.println("p.equals(cp)  = " + p.equals(cp));   // true
        System.out.println("cp.equals(p)  = " + cp.equals(p));   // false: not symmetric

        // contains calls argument.equals(element), so the answer flips with the order.
        System.out.println("List.of(p).contains(cp)  = " + List.of(p).contains(cp));
        System.out.println("List.of(cp).contains(p)  = " + List.of(cp).contains(p));
    }
}

Value equality and inheritance do not mix: make it final, or use composition.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OOP course, and every lesson in it is listed on the OOP contents page.