🧬 Inheritance & Polymorphism · Intermediate

equals() contract in Java

Reflexive, symmetric, transitive, consistent, x.equals(null) is false.

🧩 The mysteryYour list contains the point (1, 2). You ask list.contains(new Point(1, 2)) and Java says false. The point is right there! What's Java looking at?

Who calls equals?

List.contains, indexOf, remove, HashSet, HashMap — they all decide "same element?" by calling **equals. Without an override you get Object's identity check, so a different but equal-looking** object is never found.

The standard pattern

The parameter **must be Object**. Check the type with instanceof (which binds a typed variable), then compare the significant fields.

@Override
public boolean equals(Object o) {
    return o instanceof Point p
        && x == p.x && y == p.y;
}
🔮 Predict it

Found it this time

Pt overrides equals properly. What prints?

class Pt {
    int x; Pt(int x) { this.x = x; }
    public boolean equals(Object o) {
        return o instanceof Pt p && p.x == x;
    }
}
void main() {
    var ps = List.of(new Pt(1), new Pt(2));
    System.out.println(ps.contains(new Pt(2)));
}
  1. true
  2. false
  3. Compile error
Show the answer

true. contains calls equals on each element, and Pt's equals compares x. Without the override, the brand-new Pt(2) would never be found. Bonus: for null, instanceof is false, so this equals returns false without crashing.

The contract

A correct equals behaves like mathematical equality. Reflexive: x.equals(x). Symmetric: x.equals(y) matches y.equals(x). Transitive: x = y and y = z imply x = z. Consistent: same answer until the objects change. And **x.equals(null) is false** — never an exception.

⚠️ The trap

Casting without checking

This equals crashes for some inputs: the cast throws ClassCastException for other types, and p.x throws NullPointerException for null. Check first: if (!(o instanceof Point p)) return false;

@Override
public boolean equals(Object o) {
    Point p = (Point) o;  // unsafe!
    return x == p.x && y == p.y;
}
🤔 Think first

Which rule breaks?

CaseInsensitiveString.equals also accepts plain Strings, so ci.equals("abc") is true — but "abc".equals(ci) is false. Which rule is broken?

Think about it, then reveal the answer

Symmetry. String knows nothing about your class, so the comparison only works one way. Collections then behave unpredictably — contains may answer differently depending on which object is asked.

💼 In the real world

In real projects

Broken equals shows up as duplicates in sets, contains that "can't find" items, and caches that never hit. Records generate a correct equals automatically; for classes, IDEs generate the standard pattern for you.

Key takeaways

  1. Reflexive: x.equals(x) is true
  2. Symmetric and transitive, like mathematical equality
  3. x.equals(null) returns false, never throws
  4. Parameter type must be Object
🤯 Did you know?

java.sql.Timestamp famously breaks symmetry with java.util.Date: a Date can equal a Timestamp while the Timestamp says it doesn't equal the Date. Its own Javadoc warns about it.

Practice questions

What does this print?

class Pt {
    int x; Pt(int x) { this.x = x; }
    public boolean equals(Object o) {
        return o instanceof Pt p && p.x == x;
    }
}
void main() {
    System.out.println(new Pt(3).equals(new Pt(3)));
    System.out.println(new Pt(3).equals(null));
}
  1. true false
  2. false false
  3. true true
  4. Throws NullPointerException
Check your answer

true false. Two Pts with the same x are equal. For null, instanceof is false, so equals returns false without touching p.

CaseInsensitiveString.equals accepts plain Strings, so ci.equals("abc") is true but "abc".equals(ci) is false. Which rule is broken?

  1. Symmetry
  2. Reflexivity
  3. Transitivity
  4. Consistency
Check your answer

Symmetry. Symmetry requires x.equals(y) and y.equals(x) to agree. String knows nothing about the custom class, so the comparison is one-sided and collections behave unpredictably.

Next: you overrode equals — but forgot its twin. Now HashSet can't find anything.