Rules for Overriding the equals Method in Java
Implementing a custom equals method is a frequent source of latent bugs. In many cases, the healthiest choice is to leave the method untouched so that identity alone determines equality. This default behavior is correct when:
- Instances are unique by nature. Classes modeling active entities rather than values, such as
Thread, fit this pattern. The identity-based semantics provided byObject.equalsaccurately capture their behavior. - Logical comparison offers no value. The designers of
java.util.regex.Pattern, for instance, chose not to implement regular-expression equality because client code rarely needs it. Retaining the inherited implementation avoids unnecessary complexity. - A superclass already satisfies the requirement. Concrete collection implementations typically inherit appropriate logic from abstracts like
AbstractSet,AbstractList, orAbstractMap. - The class is inaccessible and never compared. For private or package-private types guaranteed to avoid equality checks, overriding
equalssolely to trap accidental invocations remains an option:
@Override
public boolean equals(Object obj) {
throw new AssertionError("Should never be called");
}
Override equals only when a class conceptually represents a value distinct from its object identity, and no ancestor has already introduced value-based comparison. Types such as Integer and String exemplify value classes: programmers expect equals to reveal whether two references denote the same logical value, not whether they point to the same memory location. Satisfying this expectation also allows enstances to function correctly as keys in hash maps or elements in sets.
Consider a simple class without a custom implementation:
class State {
String value;
}
State first = new State();
State second = new State();
System.out.println(first == second); // false
System.out.println(first.equals(second)); // false
After overriding equals to compare the meaningful field, logically equivalent instances evaluate as equal even when they are distinct objects:
class State {
String value;
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof State)) return false;
State other = (State) obj;
return java.util.Objects.equals(value, other.value);
}
}
State first = new State();
State second = new State();
System.out.println(first == second); // false
System.out.println(first.equals(second)); // true
Some value classes do not require a custom implementation. Classes employing instance control—guaranteeing at most one object per value—or enum types merge logical equality with identity. For them, Object.equals already behaves correctly.
If you do override equals, you must honor its general contract. The specification mandates that the method implement an equivalence relation with the following properties:
- Reflexive: For any non-null reference
x,x.equals(x)returnstrue. - Symmetric: For non-null references
xandy,x.equals(y)yieldstrueif and only ify.equals(x)does. - Transitive: For non-null references
x,y, andz, ifx.equals(y)andy.equals(z)aretrue, thenx.equals(z)must also betrue. - Consistent: Provided no field participating in the comparison changes, repeated calls to
x.equals(y)return the same boolean result. - Null rejection: For any non-null reference
x,x.equals(null)returnsfalse.
These constraints partition objects into equivalence classes whose members must be interchangeable from the perspective of any client. Violating any property can cause collections and other dependent classes to behave inconsistently or crash. Because objects routinely cross componant boundaries, defective equals implementations propagate bugs across modules, making strict compliance critical.