Java equals Method: Contract, Implementation, and Pitfalls
java equals method: Learn how to correctly override the Java equals method, understand the equals and hashCode contract, and avoid common pitfalls that break collections.
The default implementation of the Java equals method compares object references, not object content. For many domain objects, that behavior is insufficient, and developers override equals to define value-based equality. But overriding equals is not just about writing a comparison; it must follow the contract defined in the Object class, or your objects will behave unpredictably in collections and maps.
The Default Behavior of equals
The Object class provides a default equals implementation that uses reference equality: this == obj. This means two instances are equal only if they are the same object in memory. For value objects such as String, Integer, or custom domain classes, you typically want equality based on field values. For example, two Person objects with the same ID and name should be considered equal even if they are distinct instances.
When you do not override equals, the default behavior applies. This is often correct for objects that have a unique identity, such as a singleton or a lock object. But for objects that represent values, overriding equals is necessary to make comparisons meaningful.
The equals Contract and Its Rules
The equals method must satisfy five properties for any non-null references x, y, and z:
- Reflexive: x.equals(x) must return true.
- Symmetric: x.equals(y) must return true if and only if y.equals(x) returns true.
- Transitive: if x.equals(y) and y.equals(z) are true, then x.equals(z) must be true.
- Consistent: multiple invocations of x.equals(y) must consistently return the same result, provided no fields used in the comparison are modified.
- Non-null: x.equals(null) must return false.
These rules are not optional. Violating them causes subtle bugs, especially when objects are used as keys in HashMap or elements in HashSet. For instance, if symmetry is broken, a collection may contain duplicate elements or fail to find an existing key.
Implementing equals Correctly
A typical implementation follows a standard pattern. Consider a Person class with id, firstName, and lastName:
public class Person { private final int id; private final String firstName; private final String lastName; public Person(int id, String firstName, String lastName) { this.id = id; this.firstName = firstName; this.lastName = lastName; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Person other = (Person) obj; return id == other.id && Objects.equals(firstName, other.firstName) && Objects.equals(lastName, other.lastName); } }
The method first checks reference equality as a fast path. Then it rejects null and ensures the object is of the same class. Using getClass() instead of instanceof prevents symmetry issues when subclasses are involved. Finally, it casts and compares each field. Objects.equals provides a null-safe comparison for reference fields.
The Relationship Between equals and hashCode
When you override equals, you must also override hashCode. The contract between the two methods is: if two objects are equal according to equals, they must have the same hash code. This is essential for hash-based collections like HashMap and HashSet, which use the hash code to locate buckets.
A correct hashCode implementation for Person might look like:
@Override public int hashCode() { return Objects.hash(id, firstName, lastName); }
Using Objects.hash is convenient, but it creates an array and boxes primitives. For performance-critical code, you can compute a hash manually using a prime multiplier. The key point is that the fields used in equals must also be used in hashCode. If you omit a field, equal objects may produce different hash codes, breaking the contract.
Common Pitfalls When Overriding equals
Several mistakes repeatedly cause bugs in production code.
Using mutable fields in equals. If a field used in the equality check changes after the object is placed in a HashSet or HashMap, the object's hash code changes, and the collection can no longer find it. Prefer immutable fields or avoid using mutable state in equals.
Breaking symmetry with inheritance. If a subclass adds fields and overrides equals using instanceof, symmetry can break. For example, a Person and an Employee subclass might compare differently depending on which object calls equals. Using getClass() avoids this but prevents a subclass from being equal to its parent. There is no perfect solution; choose the approach that fits your domain.
Ignoring floating-point fields. Comparing double or float fields with == can fail due to NaN and -0.0 semantics. Use Double.compare or Float.compare instead.
Forgetting to compare all relevant fields. If two objects are equal but differ in a field that is not compared, the equality is incomplete. This often happens when a field is added later and the developer forgets to update equals.
Performance and Maintainability Considerations
The cost of equals depends on the number and type of fields compared. For simple objects, the overhead is negligible. For large objects with many fields, you can improve performance by comparing cheap fields first. For example, compare an int field before a String field, because integer comparison is faster.
For immutable objects, you can cache the hash code once it is computed. This avoids recalculating it on every hashCode call, which is beneficial when the object is used repeatedly in hash-based collections. However, caching adds a field and requires careful initialization.
Maintainability matters too. Keep equals consistent with the domain logic. If the business definition of equality changes, update both equals and hashCode together. Document the equality semantics so future maintainers understand what is being compared.
Testing Your equals Implementation
Unit tests should verify all five contract properties. Use a test framework like JUnit to write assertions for reflexivity, symmetry, transitivity, consistency, and null handling. For example:
@Test void equalsIsSymmetric() { Person p1 = new Person(1, "Alice", "Smith"); Person p2 = new Person(1, "Alice", "Smith"); assertTrue(p1.equals(p2)); assertTrue(p2.equals(p1)); }
Also test that equal objects produce the same hash code. These tests catch regressions early and document the intended behavior.
When Not to Override equals
There are cases where the default reference equality is the right choice. If your objects represent unique entities with a database-generated ID, and you never compare them by value, overriding equals adds complexity without benefit. Similarly, if objects are used as locks or in concurrency contexts, reference equality is correct.
Overriding equals is a deliberate design decision. It should be done only when value equality is meaningful and when the object's lifecycle supports the contract. If you are unsure, start without overriding and add it when the need arises.