Skip to content

Equality, Hashing, and Immutability

Equality contracts

For non-null references, equals must be reflexive, symmetric, transitive, and consistent, and must return false for null. Equal objects must have equal hash codes during an execution.

Identity (==) asks whether references denote the same object. Value equality (equals) asks whether objects represent the same value under their contract.

record BookId(String value) {
    BookId {
        Objects.requireNonNull(value, "value");
        if (value.isBlank()) throw new IllegalArgumentException("blank id");
    }
}

A record derives equality and hashing from its components. The record is only as deeply immutable as those components; a component referring to a mutable list would still require defensive copying.

Designing immutable classes

  • make invariant-bearing state private and final;
  • validate construction;
  • do not expose mutable internals;
  • copy mutable inputs and outputs where ownership is not transferred;
  • prevent subclass mutation when the contract requires it.

Immutability simplifies reasoning, safe publication, hashing, and sharing across threads. It does not automatically make an operation atomic across multiple immutable values.

The practical guide expands these rules to shallow copies, unmodifiable views, records, builders, snapshots, and cache keys.

Comparator consistency

Sorted sets and maps use comparison to determine key distinctness. If a comparator reports zero for objects that equals considers different, the collection may appear to “lose” a key. Either make the ordering consistent with equality or document the alternative equivalence relation.

Exercises

  1. Explain why subclass-based equality often breaks symmetry.
  2. Make a class containing a List<String> deeply immutable.
  3. Describe the failure caused by mutating a hash key after insertion.