Inheritance & Polymorphism

Overriding equals & hashCode

15 min Lesson 4 of 14

Overriding equals & hashCode

In lesson 3 you learned how to override methods so a subclass can replace a parent's behaviour. Two methods that almost every real-world class eventually needs to override are equals and hashCode, both inherited from Object. Understanding why you need to override them — and the rules you must follow when you do — is essential Java knowledge.

Reference equality vs value equality

The == operator in Java checks reference equality: are these two variables pointing at the exact same object in memory? This is almost never what you want when comparing two objects that represent the same concept.

String a = new String("hello"); String b = new String("hello"); System.out.println(a == b); // false — different objects in memory System.out.println(a.equals(b)); // true — same characters

The equals method, when properly overridden, expresses value equality: do these two objects represent the same thing, even if they are different instances?

The default equals from Object

If you do not override equals, Java uses the default from Object, which simply does ==. Two distinct instances are therefore never equal, no matter how identical their fields are.

public class Point { int x; int y; public Point(int x, int y) { this.x = x; this.y = y; } } Point p1 = new Point(3, 4); Point p2 = new Point(3, 4); System.out.println(p1.equals(p2)); // false — Object's default equals is used

This is rarely the right answer for domain objects. Two points at (3, 4) should be considered equal.

Overriding equals correctly

The equals contract (from the Java documentation) requires five properties: reflexive (x.equals(x) is always true), symmetric (x.equals(y) implies y.equals(x)), transitive, consistent (repeated calls return the same result), and x.equals(null) always returns false.

public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object obj) { if (this == obj) return true; // same reference — trivially equal if (obj == null) return false; // never equal to null if (getClass() != obj.getClass()) return false; // different types Point other = (Point) obj; // safe cast now return this.x == other.x && this.y == other.y; } } Point p1 = new Point(3, 4); Point p2 = new Point(3, 4); System.out.println(p1.equals(p2)); // true
Why check getClass() instead of instanceof? Using instanceof can break symmetry when subclasses are involved: p1.equals(coloredPoint) might be true while coloredPoint.equals(p1) is false. getClass() keeps both directions consistent. For value-based records and sealed classes, instanceof is fine; for regular inheritance hierarchies, prefer getClass().

Why you must also override hashCode

Java's collections — HashMap, HashSet, Hashtable — rely on hashCode to place and find objects in buckets. The rule is strict:

  • If a.equals(b) is true, then a.hashCode() must equal b.hashCode().
  • If a.hashCode() != b.hashCode(), then a.equals(b) must be false (they cannot be equal).
  • Two objects that are not equal are allowed to have the same hash code (a collision), but fewer collisions means better performance.

If you override equals without overriding hashCode, equal objects can have different hash codes, and a HashSet will treat two logically identical objects as separate entries.

// Without overriding hashCode Set<Point> set = new HashSet<>(); set.add(new Point(3, 4)); System.out.println(set.contains(new Point(3, 4))); // false! Bug — wrong hashCode

Overriding hashCode

A straightforward and collision-resistant approach uses Objects.hash(), which computes a combined hash of all the fields used in equals:

import java.util.Objects; @Override public int hashCode() { return Objects.hash(x, y); // same fields as equals }

With both methods in place:

Set<Point> set = new HashSet<>(); set.add(new Point(3, 4)); System.out.println(set.contains(new Point(3, 4))); // true — correct

A complete example

import java.util.Objects; public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Point other = (Point) obj; return x == other.x && y == other.y; } @Override public int hashCode() { return Objects.hash(x, y); } @Override public String toString() { return "Point(" + x + ", " + y + ")"; } }
Modern shortcut — records: Java 16+ records generate correct equals, hashCode, and toString automatically. record Point(int x, int y) {} gives you all three for free. For plain classes you still write them by hand or let your IDE generate them.
Never base hashCode on mutable fields. If you put an object in a HashSet, then change a field that is part of its hash, the set can no longer find it — the object is lost in the wrong bucket. Make the fields used in equals and hashCode immutable (or at least stable) whenever possible.

Summary

  • == checks reference equality; equals checks value equality.
  • The default equals from Object is just == — override it for domain objects.
  • Follow the five-property equals contract: reflexive, symmetric, transitive, consistent, null-safe.
  • Always override hashCode when you override equals — they must agree.
  • Use Objects.hash(field1, field2, ...) for a clean, reliable hashCode implementation.