equals() contract in Java
Reflexive, symmetric, transitive, consistent, x.equals(null) is false.
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;
}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)));
}truefalseCompile 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.
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;
}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 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
- Reflexive: x.equals(x) is true
- Symmetric and transitive, like mathematical equality
- x.equals(null) returns false, never throws
- Parameter type must be Object
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));
}- true false
- false false
- true true
- 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?
- Symmetry
- Reflexivity
- Transitivity
- 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.