Memra

equals & hashCode done right

The full contract, a correct hand-written pair, getClass vs instanceof, and how record makes it free.

The equals/hashCode contract

Java's Object gives every class two related methods: equals and hashCode. The default implementations use object identity (pointer comparison), which is almost never what you want for domain objects. The exam and every real codebase expect you to override them correctly.

The contract has three obligations:

  1. Consistency: if two objects are equals, they must have the same hashCode.
  2. Hash stability: hashCode must return the same value on multiple calls as long as the object's state hasn't changed.
  3. equals contract: reflexive (x.equals(x) is true), symmetric (x.equals(y)y.equals(x)), transitive, and consistent. x.equals(null) must return false, never throw.

Breaking rule 1 silently destroys HashMap and HashSet — the object can be put in but never found again.

A correct hand-written pair

import java.util.Objects;

public final 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 o) {
        if (this == o) return true;
        if (!(o instanceof Point other)) return false;
        return x == other.x && y == other.y;
    }

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

Objects.hash(x, y) — handles primitives (auto-boxed) and nulls. Never write the 31-prime multiplication by hand — Objects.hash is idiomatic and less error-prone.

getClass() vs instanceof

The pattern !(o instanceof Point other) allows subclasses of Point to be equal to a Point. That satisfies symmetry only if the subclass doesn't add fields that affect equality. If it does, symmetry breaks.

The safer alternative for classes that may be subclassed is getClass():

if (o == null || getClass() != o.getClass()) return false;

This guarantees only same-class instances are ever equal. For final classes (like Point above), the question is moot — nothing can subclass them, so instanceof is fine.

records make it free

Java 16's record generates a correct equals and hashCode over all record components automatically:

public record Point(int x, int y) {}
// equals, hashCode, and toString are all synthesised

For immutable value objects with no custom logic, prefer record and skip the hand-written pair entirely.

NORMAL ~/memra/learn/java-from-zero/equals-and-hashcode utf-8 LF