Back to Blog
Java

Java equals Method Override: Contract and Pitfalls

java equals method override: Learn how to override equals() in Java correctly: the equals contract, hashCode consistency, inheritance pitfalls, and field comparison ru...

equals contracthashCodeJava collectionsinheritanceobject equality
Two Java objects compared for equality on a balanced scale, representing the equals contract and hashCode consistency.

When a Java class overrides equals(), it takes responsibility for defining what value equality means for that type. The default Object.equals() implementation compares references, which is rarely the correct behavior for domain objects. But the java equals method override is more than writing a method that compares fields — the override must satisfy the equals contract, remain consistent with hashCode(), and behave correctly when the class participates in inheritance.

The equals Contract Every Override Must Follow

The Object class documentation defines five properties that every equals() implementation must satisfy:

  • Reflexive: x.equals(x) must return true.
  • Symmetric: x.equals(y) must return the same result as y.equals(x).
  • Transitive: If x.equals(y) and y.equals(z) are both true, then x.equals(z) must also be true.
  • Consistent: Repeated calls must return the same result, assuming no relevant fields change.
  • Non-null: x.equals(null) must return false.

These properties are not academic. Collections like HashSet, HashMap, and ArrayList rely on them during contains(), remove(), and key lookup. A violation can cause elements to be unfindable, duplicates to appear, or lookups to return incorrect results.

The Standard Pattern for Overriding equals

A typical override follows a recognizable pattern:

@Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Person other = (Person) obj; return age == other.age && Objects.equals(name, other.name) && Objects.equals(email, other.email); }

The reference check (this == obj) is a fast path that avoids field comparison when the same instance is compared to itself. The null check prevents a NullPointerException during the cast. Using getClass() instead of instanceof ensures that a subclass instance is never considered equal to a superclass instance, which preserves symmetry in inheritance hierarchies.

The field comparisons use Objects.equals() for nullable references and == for primitives. Objects.equals() handles null safely, so a null name field compares correctly against another null name field.

Why hashCode Must Change With equals

Java's hash-based collections — HashMap, HashSet, Hashtable — store entries in buckets determined by hashCode(). When two objects are equal, they must produce the same hash code. Otherwise, a lookup that starts by computing the hash may search the wrong bucket and never find the stored entry.

@Override public int hashCode() { return Objects.hash(age, name, email); }

Objects.hash() combines the hash codes of the same fields used in equals(). The rule is simple: every field that participates in equals() must participate in hashCode(), and fields excluded from equals() should not appear in hashCode().

The reverse direction is also worth noting: two unequal objects may share the same hash code. That is called a collision and is handled by the bucket structure. But two equal objects with different hash codes break the contract and produce observable bugs.

Common Mistakes That Break equals

One frequent error is comparing the wrong type. If the method signature uses the class itself instead of Object, it is an overload, not an override:

// This is an overload, not an override public boolean equals(Person other) { return this.name.equals(other.name); }

Because the parameter type is Person, this method does not override Object.equals(Object). Any collection that calls equals(Object) — which is what List.contains() and Map.get() do — will fall back to reference equality.

Another mistake is using mutable fields in equals(). If a field changes after the object is placed in a HashSet, the object's hash code changes, and the set can no longer locate the entry. The object remains in the set but becomes effectively lost.

A third common issue is asymmetric equals across inheritance. If a subclass overrides equals() using instanceof and adds fields, comparing a parent instance with a child instance can return true in one direction and false in the other, violating symmetry.

Handling Inheritance: getClass vs instanceof

The choice between getClass() and instanceof in the type check changes the semantics of equality across subclasses.

Using getClass() means a Person and a subclass like Employee are never equal, even if all shared fields match. This preserves symmetry and transitivity but makes equality stricter.

Using instanceof allows a subclass instance to be equal to a parent instance, but only if the subclass does not add fields to the comparison. If the subclass adds a field and includes it in equals(), symmetry breaks: parent.equals(child) may be true while child.equals(parent) is false, because the parent does not know about the child's extra field.

A common workaround for the instanceof approach is to compare only fields defined in the common superclass, but this creates a different problem: two Employee objects with different salaries would compare equal. That is rarely the intended behavior.

The practical recommendation: use getClass() when subclasses add fields that participate in equality, and use instanceof only when the hierarchy is designed so that all subclasses share the same equality semantics.

Performance Considerations in equals Implementation

equals() is called frequently in collection operations. A HashSet.contains() call on a large set may invoke equals() on every element in a bucket. The implementation should avoid expensive work in the common path.

The reference check at the top is a cheap optimization. Ordering field comparisons by cost and likelihood of divergence helps: compare fields that are most likely to differ first, and compare expensive fields — like large collections or strings — last.

For example, comparing an integer ID before comparing a long string field short-circuits the comparison when IDs differ:

@Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Order other = (Order) obj; return id == other.id && Objects.equals(customerName, other.customerName); }

This ordering matters most when the class is used in hot paths, such as deduplication of large streams or repeated lookups in hash collections. For most domain objects, the difference is negligible, but the habit of ordering cheap, discriminating fields first costs nothing and avoids accidental performance regressions.

Comparing Fields: Primitives, Objects, and Arrays

Field comparison rules differ by type:

  • Primitives (int, long, boolean): use ==.
  • Floating-point primitives: use Float.compare() or Double.compare() instead of ==. The == operator treats +0.0 and -0.0 as equal, but Float.hashCode() and Double.hashCode() assign them different hash codes, which violates the equals/hashCode contract. Float.compare() treats them as distinct, matching the hashCode behavior.
  • References: use Objects.equals() for null-safe comparison.
  • Arrays: use Arrays.equals() for content comparison. The array's own equals() is reference equality, so comparing two arrays with == or Objects.equals() checks identity, not content.
@Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Record other = (Record) obj; return Arrays.equals(tags, other.tags) && Objects.equals(owner, other.owner); }

For nested arrays or collections, Arrays.deepEquals() handles multidimensional arrays, and Objects.deepEquals() delegates appropriately for arrays of any depth.

java equals method override: Practical Usage and Code Exampl | RYUSLOG DEV