Lesson 19 / 25

Identity, Equality and Hashing

Implement equals and hashCode correctly and understand identity versus value equality.

Same object or equal objects?

Identity asks whether two references point to the same object (== for objects in Java, is in Python). Equality asks whether two objects represent the same value (equals in Java, == via __eq__ in Python). By default, Java's Object.equals uses identity, so two Money(500, "INR") objects are not equal unless you override it. The equals contract requires equality to be reflexive, symmetric, transitive and consistent, and x.equals(null) must be false. Crucially, if you override equals you must override hashCode so that equal objects have equal hash codes; otherwise hash-based collections (HashMap, HashSet) misbehave, failing to find keys that are "equal". Base equality on fields that define identity in the domain, and avoid mutable fields in objects used as hash keys. Java records and Kotlin data classes generate correct equals and hashCode automatically; in Python, defining __eq__ without __hash__ makes instances unhashable, and @dataclass(frozen=True) generates both.

Identity versus equality

Two references to one object are identical; two objects with the same value are equal but not identical.

Left: two arrows pointing to one box. Right: two separate boxes with identical contents, each with its own arrow.
Figure 7.1 — Identity (same object) compared with equality (same value).

equals and hashCode done right

Equal objects must produce equal hash codes.

import java.util.Objects;

public final class Money {
    private final long paise;
    private final String currency;

    public Money(long paise, String currency) {
        this.paise = paise;
        this.currency = Objects.requireNonNull(currency);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money other)) return false;
        return paise == other.paise && currency.equals(other.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(paise, currency);
    }
}

var set = new java.util.HashSet<Money>();
set.add(new Money(500, "INR"));
boolean found = set.contains(new Money(500, "INR"));   // true only because hashCode is overridden too

// equivalent and shorter:
// public record Money(long paise, String currency) { }

Never compare strings with == in Java

== on strings compares references; it may appear to work for literals because of string interning, then fail for strings read from input. Use equals (or Objects.equals to handle nulls).

Quick check: If a Java class overrides equals but not hashCode, what typically goes wrong?

  • Equal objects may land in different hash buckets, so HashSet and HashMap lookups fail
  • The class no longer compiles
  • equals stops being called
  • Objects become immutable
Answer

Equal objects may land in different hash buckets, so HashSet and HashMap lookups fail — The equals-hashCode contract requires equal objects to have equal hash codes.