Stage 2: Object-oriented programming, lesson 9 of 10

equals(), hashCode() and the Object contract

Intermediate3 min read@since 16Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

Every class extends java.lang.Object, which provides equals(), hashCode(), toString(), getClass() and a few more.

By default, equals() compares references. Override it when two objects holding the same data should count as equal.

The contract: if a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true. Break it, and HashMap and HashSet will lose your objects.

Use Objects.equals and Objects.hash (Java 7) to write these safely, or let a record generate them for you.

The methods every object has

Every class extends Object, inheriting equals, hashCode, toString, getClass, clone, wait and notify. finalize is deprecated for removal; use try-with-resources or a Cleaner to release resources.

The equals contract

equals must be reflexive (a.equals(a)), symmetric, transitive, consistent, and return false for null. Compare the fields that define identity, and use getClass() or a final class to keep symmetry with subclasses.

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

The hashCode contract

Objects that are equals must have the same hashCode, or HashMap and HashSet will lose them. Unequal objects may share a hash, but fewer collisions is faster. Objects.hash combines fields for you.

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

A useful toString

The default toString prints the class name and a hash (Point@1b6d3586). Override it with the fields that help debugging and logs, but never include passwords or tokens.

Copying objects: clone vs copy constructors

clone() needs the Cloneable marker interface, makes a shallow copy by default (nested objects are shared) and is awkward to get right. Prefer a copy constructor or a static factory, and copy nested mutable objects yourself (a deep copy).

Java
public class Cart {
    private final List<String> items;
    public Cart(List<String> items) { this.items = new ArrayList<>(items); }
    public Cart(Cart other) { this(other.items); }   // copy constructor (deep enough here)
}

Records do it for you

A record generates equals, hashCode and toString from its components, correctly. For immutable data, a record is the simplest way to follow the Object contract.

Java
record Point(int x, int y) {}
new Point(1, 2).equals(new Point(1, 2));   // true
new Point(1, 2).toString();                 // "Point[x=1, y=2]"

Example

Java
public final class Isbn {
    private final String code;
    public Isbn(String code) { this.code = code; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Isbn other)) return false;   // pattern matching, Java 16
        return code.equals(other.code);
    }

    @Override
    public int hashCode() { return Objects.hash(code); }

    @Override
    public String toString() { return "Isbn[" + code + "]"; }
}

Set<Isbn> set = new HashSet<>();
set.add(new Isbn("978-0134685991"));
System.out.println(set.contains(new Isbn("978-0134685991")));   // true

Common mistake

Overriding equals but not hashCode. Two equal objects land in different HashMap buckets and contains() returns false.

Under the hood

For JPA entities this gets subtle: a generated id is null before the entity is saved, so a hash based on the id changes after persisting. Common approaches are a natural business key, or id-based equals with a constant hashCode() such as getClass().hashCode(). Never include lazy associations in equals, hashCode or toString; that triggers extra queries or LazyInitializationException.

Check yourself

If a.equals(b) is true, which must also be true?

How this connects

Part of Job-ready backend developer, Crack the Java interview.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.