Java

equals, hashCode, and the contract between them

Why overriding one without the other breaks every hash-based collection silently.

After this lesson you can

  • Override equals and hashCode together, over the same fields
  • State the contract between them in one sentence
  • Explain why a HashSet can hold two objects that are equal to each other

The default equals (inherited from Object) compares references — a.equals(b) is true only when a and b are the exact same object. For a value type, that is almost never what you want.

public final class Point {
    private final int x, y;

    public Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point other)) return false;
        return x == other.x && y == other.y;
    }
}

This alone already breaks something. HashSet, HashMap, and every hash-based collection find a bucket using hashCode() before they ever call equals(). Point above still has Object's identity hashCode, so two equal points almost always land in different buckets, equals is never consulted, and a HashSet<Point> ends up holding both.

Try it

Two equal points, one broken HashSetjava-21
import java.util.*; public class Solution {    static final class BrokenPoint {        final int x, y;        BrokenPoint(int x, int y) { this.x = x; this.y = y; }         @Override        public boolean equals(Object o) {            if (this == o) return true;            if (!(o instanceof BrokenPoint other)) return false;            return x == other.x && y == other.y;        }        // hashCode is NOT overridden — still the identity hash.    }     public static String describe() {        Set<BrokenPoint> set = new HashSet<>();        set.add(new BrokenPoint(1, 2));        set.add(new BrokenPoint(1, 2));        return "size=" + set.size();    }}
What to look for

The contract

Java's actual rule, from Object's documentation: if two objects are equal according to equals, they must return the same hashCode. The reverse is not required — unequal objects may share a hash code, which is just a collision, and every hash-based collection already handles collisions.

@Override
public int hashCode() {
    return Objects.hash(x, y);
}

Objects.hash(...) combines any number of fields into one hash code correctly, which is why hand-rolling one is rarely worth it. The two methods must be overridden together, over the same fields — a hashCode that reads a field equals ignores (or the reverse) breaks the contract just as surely as leaving one of them out entirely.

Why the immutability matters

If a field used in hashCode changes after the object is inside a HashSet or used as a HashMap key, its bucket does not move — the object is now sitting in the wrong bucket for its current hash code, and a lookup for it, computed from its current state, will look in the bucket it should be in and not find it. This is the practical reason a key type should be immutable: correctness inside a hash-based collection depends on the hash code never changing while the object is a member of one.

A Java record generates a correct equals, hashCode, and toString for you, over exactly its declared fields, which is why a plain value type is usually better written as a record than as a hand rolled class.

Try it yourself

2 visible tests · 2 hidden tests

Point below has equals but not hashCode, so a HashSet<Point> can hold two points that are equal to each other. Add a correct hashCode, over the same two fields equals uses. Implement distinctCount(coords), where coords is a List<List<Integer>> of [x, y] pairs — build a Point from each pair, add them all to a HashSet<Point>, and return its size.

  • distinctCount([[1,2],[1,2],[3,4]])
  • distinctCount([[0,0],[1,1],[2,2]])
Loading editor…

Sign up to check the hidden tests and save your progress. Sign up