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
- Use getClass() for exact type matching
- Call super.equals() first in subclass
- Compare all new fields in subclass
- Override hashCode() too
- Test symmetry and transitivity
- 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:
- getClass() for exact type matching
- Call super.equals() in subclass
- Symmetry:
x.equals(y)=y.equals(x) - Transitivity:
x.equals(y)&&y.equals(z)→x.equals(z) - Reflexive:
x.equals(x)always true - instanceof breaks symmetry with new fields
- Override hashCode() with equals()
- Compare all fields including parent’s
- Test all properties of equals()
- 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?