Equality Testing and Inheritance

Introduction

Equality testing in inheritance hierarchies requires careful implementation to maintain symmetry and transitivity.


The Problem

class Employee {
    String name;
    double salary;

    Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || getClass() != obj.getClass()) return false;
        Employee emp = (Employee) obj;
        return name.equals(emp.name) && salary == emp.salary;
    }
}

class Manager extends Employee {
    String department;

    Manager(String name, double salary, String department) {
        super(name, salary);
        this.department = department;
    }

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;
        if (getClass() != obj.getClass()) return false;
        Manager mgr = (Manager) obj;
        return department.equals(mgr.department);
    }
}

public class Main {
    public static void main(String[] args) {
        Employee emp = new Employee("John", 50000);
        Manager mgr = new Manager("John", 50000, "IT");

        System.out.println(emp.equals(mgr));  // false (different classes)
        System.out.println(mgr.equals(emp));  // false (different classes)
    }
}

Symmetry Requirement

If x.equals(y), then y.equals(x) must be true.

Broken Symmetry:

class Employee {
    String name;

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Employee)) return false;
        Employee emp = (Employee) obj;
        return name.equals(emp.name);
    }
}

class Manager extends Employee {
    String department;

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Manager)) return false;
        Manager mgr = (Manager) obj;
        return super.equals(obj) && department.equals(mgr.department);
    }
}

public class Main {
    public static void main(String[] args) {
        Employee emp = new Employee("John");
        emp.name = "John";

        Manager mgr = new Manager("John", 50000, "IT");
        mgr.name = "John";

        System.out.println(emp.equals(mgr));  // true (Employee check)
        System.out.println(mgr.equals(emp));  // false (Manager check)
        // Symmetry broken!
    }
}

Solution 1: Use getClass()

Only compare objects of exact same class.

class Employee {
    String name;
    double salary;

    Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;

        // Use getClass() for exact type match
        if (getClass() != obj.getClass()) return false;

        Employee emp = (Employee) obj;
        return name.equals(emp.name) && salary == emp.salary;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, salary);
    }
}

class Manager extends Employee {
    String department;

    Manager(String name, double salary, String department) {
        super(name, salary);
        this.department = department;
    }

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;
        Manager mgr = (Manager) obj;
        return department.equals(mgr.department);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, salary, department);
    }
}

public class Main {
    public static void main(String[] args) {
        Employee emp = new Employee("John", 50000);
        Manager mgr = new Manager("John", 50000, "IT");

        System.out.println(emp.equals(mgr));  // false
        System.out.println(mgr.equals(emp));  // false
        // Symmetry maintained
    }
}

Solution 2: instanceof (Limited Use)

Only when subclass has no new fields.

class Point {
    int x, y;

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

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) return false;
        Point p = (Point) obj;
        return x == p.x && y == p.y;
    }
}

class ColoredPoint extends Point {
    // No new fields for comparison!
    String color;  // Only for display

    ColoredPoint(int x, int y, String color) {
        super(x, y);
        this.color = color;
    }

    // Don't override equals() if color doesn't matter for equality
}

public class Main {
    public static void main(String[] args) {
        Point p = new Point(1, 2);
        ColoredPoint cp = new ColoredPoint(1, 2, "Red");

        System.out.println(p.equals(cp));   // true
        System.out.println(cp.equals(p));   // true
        // OK because only comparing x, y
    }
}

Complete Example: Bank Account

import java.util.Objects;

class BankAccount {
    protected String accountNumber;
    protected double balance;

    BankAccount(String accountNumber, double balance) {
        this.accountNumber = accountNumber;
        this.balance = balance;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        BankAccount account = (BankAccount) obj;
        return accountNumber.equals(account.accountNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountNumber);
    }
}

class SavingsAccount extends BankAccount {
    private double interestRate;

    SavingsAccount(String accountNumber, double balance, double interestRate) {
        super(accountNumber, balance);
        this.interestRate = interestRate;
    }

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;
        SavingsAccount sa = (SavingsAccount) obj;
        return Double.compare(interestRate, sa.interestRate) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountNumber, interestRate);
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount ba = new BankAccount("A001", 1000);
        SavingsAccount sa1 = new SavingsAccount("A001", 1000, 4.5);
        SavingsAccount sa2 = new SavingsAccount("A001", 1500, 4.5);

        // Different classes
        System.out.println("ba.equals(sa1): " + ba.equals(sa1));    // false
        System.out.println("sa1.equals(ba): " + sa1.equals(ba));    // false

        // Same class, same account and rate
        System.out.println("sa1.equals(sa2): " + sa1.equals(sa2));  // true
    }
}

Transitivity Problem

If x.equals(y) and y.equals(z), then x.equals(z).

Broken Transitivity:

class Point {
    int x, y;

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) return false;
        Point p = (Point) obj;
        return x == p.x && y == p.y;
    }
}

class ColoredPoint extends Point {
    String color;

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) return false;

        // If comparing with plain Point, ignore color
        if (!(obj instanceof ColoredPoint)) {
            return super.equals(obj);
        }

        // If comparing with ColoredPoint, check color too
        ColoredPoint cp = (ColoredPoint) obj;
        return super.equals(obj) && color.equals(cp.color);
    }
}

public class Main {
    public static void main(String[] args) {
        ColoredPoint red = new ColoredPoint();
        red.x = 1; red.y = 2; red.color = "Red";

        Point point = new Point();
        point.x = 1; point.y = 2;

        ColoredPoint blue = new ColoredPoint();
        blue.x = 1; blue.y = 2; blue.color = "Blue";

        System.out.println(red.equals(point));   // true
        System.out.println(point.equals(blue));  // true
        System.out.println(red.equals(blue));    // false
        // Transitivity broken!
    }
}

Best Practice: Composition over Inheritance

If equality depends on new fields, consider composition.

class Point {
    int x, y;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        Point p = (Point) obj;
        return x == p.x && y == p.y;
    }
}

class ColoredPoint {
    private Point point;  // Composition
    private String color;

    ColoredPoint(int x, int y, String color) {
        this.point = new Point(x, y);
        this.color = color;
    }

    Point getPoint() {
        return point;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        ColoredPoint cp = (ColoredPoint) obj;
        return point.equals(cp.point) && color.equals(cp.color);
    }
}

Guidelines for equals() in Inheritance

  1. Use getClass() for exact type matching
  2. Call super.equals() first in subclass
  3. Compare all new fields in subclass
  4. Override hashCode() too
  5. Test symmetry and transitivity
  6. Consider composition if equality is complex

Testing Equality

class Employee {
    String name;

    Employee(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        Employee emp = (Employee) obj;
        return name.equals(emp.name);
    }
}

class Manager extends Employee {
    String department;

    Manager(String name, String department) {
        super(name);
        this.department = department;
    }

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;
        Manager mgr = (Manager) obj;
        return department.equals(mgr.department);
    }
}

public class Main {
    public static void main(String[] args) {
        Employee e1 = new Employee("John");
        Employee e2 = new Employee("John");
        Manager m1 = new Manager("John", "IT");
        Manager m2 = new Manager("John", "IT");
        Manager m3 = new Manager("John", "HR");

        // Test reflexive: x.equals(x)
        System.out.println("Reflexive e1: " + e1.equals(e1));  // true
        System.out.println("Reflexive m1: " + m1.equals(m1));  // true

        // Test symmetric: x.equals(y) = y.equals(x)
        System.out.println("Symmetric e1-e2: " +
                         (e1.equals(e2) == e2.equals(e1)));  // true
        System.out.println("Symmetric m1-m2: " +
                         (m1.equals(m2) == m2.equals(m1)));  // true

        // Test transitive: x.equals(y) && y.equals(z) => x.equals(z)
        Employee e3 = new Employee("John");
        System.out.println("e1.equals(e2): " + e1.equals(e2));  // true
        System.out.println("e2.equals(e3): " + e2.equals(e3));  // true
        System.out.println("e1.equals(e3): " + e1.equals(e3));  // true

        // Different class types not equal
        System.out.println("e1.equals(m1): " + e1.equals(m1));  // false
        System.out.println("m1.equals(e1): " + m1.equals(e1));  // false
    }
}

Real-World Example: Shape Hierarchy

import java.util.Objects;

abstract class Shape {
    protected String color;

    Shape(String color) {
        this.color = color;
    }

    abstract double area();

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        Shape shape = (Shape) obj;
        return color.equals(shape.color);
    }

    @Override
    public int hashCode() {
        return Objects.hash(color);
    }
}

class Circle extends Shape {
    private double radius;

    Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;
        Circle circle = (Circle) obj;
        return Double.compare(radius, circle.radius) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(color, radius);
    }
}

class Rectangle extends Shape {
    private double length, width;

    Rectangle(String color, double length, double width) {
        super(color);
        this.length = length;
        this.width = width;
    }

    @Override
    double area() {
        return length * width;
    }

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;
        Rectangle rect = (Rectangle) obj;
        return Double.compare(length, rect.length) == 0 &&
               Double.compare(width, rect.width) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(color, length, width);
    }
}

public class Main {
    public static void main(String[] args) {
        Circle c1 = new Circle("Red", 5);
        Circle c2 = new Circle("Red", 5);
        Circle c3 = new Circle("Blue", 5);

        Rectangle r1 = new Rectangle("Red", 4, 6);

        System.out.println("c1.equals(c2): " + c1.equals(c2));  // true
        System.out.println("c1.equals(c3): " + c1.equals(c3));  // false
        System.out.println("c1.equals(r1): " + c1.equals(r1));  // false
    }
}

Common Mistakes

Mistake 1: Using instanceof with New Fields

class Point {
    int x, y;

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) return false;  // ✗ Problem
        Point p = (Point) obj;
        return x == p.x && y == p.y;
    }
}

class ColoredPoint extends Point {
    String color;

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ColoredPoint)) return false;
        ColoredPoint cp = (ColoredPoint) obj;
        return super.equals(obj) && color.equals(cp.color);
    }
}
// Breaks symmetry!

Mistake 2: Not Calling super.equals()

class Manager extends Employee {
    String department;

    @Override
    public boolean equals(Object obj) {
        // ✗ Missing super.equals(obj)
        if (getClass() != obj.getClass()) return false;
        Manager mgr = (Manager) obj;
        return department.equals(mgr.department);
    }
}

Quick Reference

// Parent class
class Parent {
    String field1;

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        Parent p = (Parent) obj;
        return field1.equals(p.field1);
    }

    @Override
    public int hashCode() {
        return Objects.hash(field1);
    }
}

// Child class
class Child extends Parent {
    String field2;

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;  // Call parent first

        Child c = (Child) obj;
        return field2.equals(c.field2);
    }

    @Override
    public int hashCode() {
        return Objects.hash(field1, field2);
    }
}

Exam Tips

Remember:

  1. getClass() for exact type matching
  2. Call super.equals() in subclass
  3. Symmetry: x.equals(y) = y.equals(x)
  4. Transitivity: x.equals(y) && y.equals(z)x.equals(z)
  5. Reflexive: x.equals(x) always true
  6. instanceof breaks symmetry with new fields
  7. Override hashCode() with equals()
  8. Compare all fields including parent’s
  9. Test all properties of equals()
  10. Consider composition for complex equality

Common Questions:

  • What is symmetry in equals()?
  • What is transitivity?
  • Why use getClass() over instanceof?
  • How to override equals() in subclass?
  • What are equals() contract rules?
  • How to maintain symmetry in inheritance?
  • When does instanceof break equality?
  • Best practices for equals() in inheritance?