skip to content

How does a record's generated equals, hashCode, and toString behave, and what is the resulting equality contract?

level: middleimportance: must knowfreq 70%

answer

  1. equal type + all components equal => equals true
  2. hashCode from all components, consistent with equals
  3. toString = Name[c1=.., c2=..]
  4. floats: NaN==NaN, 0.0 != -0.0
  5. array component compares by identity, not contents

basics

~20 s

Two records are equal if they are the same record type and every component is equal. hashCode is computed from all components and stays consistent with equals. toString prints the type name and each component, like Point[x=1, y=2].

solid answer

~40 s

A record's `equals` is value-based: instances are equal only if they are exactly the same record type and each corresponding component compares equal (primitives by value, references via their own `equals`). This automatically satisfies the equals contract — reflexive, symmetric, transitive, consistent — and `equals(null)` is false. `hashCode` is derived from all components so the equals/hashCode invariant holds: equal records always produce equal hash codes, making records safe HashMap/HashSet keys. `toString` yields a deterministic form like `Point[x=1, y=2]`. Because the implementations follow the component list, adding or removing a component automatically updates all three. Two caveats: floating-point components use `Double.compare`/`Float.compare` semantics (so `-0.0` differs from `0.0` and `NaN` equals `NaN`), and array components compare by reference identity, not contents — an array component usually makes a record a poor key.

code

java · 14 lines
java
record Range(int lo, int hi) { }

Range a = new Range(1, 5);
Range b = new Range(1, 5);
Range c = new Range(1, 6);

a.equals(b);                 // true  (same type, all components equal)
a.equals(c);                 // false (hi differs)
a.hashCode() == b.hashCode();// true  (equals -> equal hashCodes)
a.toString();                // "Range[lo=1, hi=5]"

// Array-component pitfall:
record Bytes(byte[] data) { }
new Bytes(new byte[]{1,2}).equals(new Bytes(new byte[]{1,2})); // false! (identity)

go deeper

for a junior

Knows that two records with the same component values are equal and that toString prints the components.

for a middle

Explains the full value-based equality, the equals/hashCode consistency that makes records safe keys, and the toString format; can describe the array-component pitfall.

for a senior

Maps the generated behavior onto the formal equals/hashCode contracts, explains floating-point and array edge cases, and knows when/how to override safely while preserving consistency.

for a principal

Reasons about value-based equality as a substitutability guarantee, defensive-copy strategies for mutable components, and the implications for caching, deduplication, and using records across serialization or distributed boundaries.

## The three generated methods Every record inherits, from `java.lang.Object`, three methods that the compiler **overrides automatically** based on the record's components. ### `equals(Object)` — value-based equality *Equality* asks 'do these two objects represent the same value?', as opposed to *identity* (the `==` operator: 'are these the same object in memory?'). A record's generated `equals` returns `true` only when: 1. The other object is of the **exact same record type**, and 2. **Each component is equal** — primitive components compared by value, reference components compared with *their* `equals` method. This guarantees the formal **equals contract** automatically: - **Reflexive:** `a.equals(a)` is true. - **Symmetric:** `a.equals(b) == b.equals(a)`. - **Transitive:** if `a.equals(b)` and `b.equals(c)` then `a.equals(c)`. - **Consistent:** repeated calls give the same result if nothing changes (and records are immutable, so nothing changes). - `a.equals(null)` is always `false`. ### `hashCode()` — consistent with equals A *hash code* is an `int` summarizing an object, used to bucket it in hash-based collections (`HashMap`, `HashSet`). The contract: **equal objects must have equal hash codes** (unequal objects *may* collide). The record's generated `hashCode` is computed from all components, so this invariant holds by construction. This is what makes records excellent **keys**: build a key, look it up later with a freshly constructed equal key, and the collection finds it. ### `toString()` Generates a readable, deterministic string: the record name, then each component as `name=value` inside square brackets — e.g. `Point[x=1, y=2]`. Useful for logging and debugging. ## Why deriving from components matters Because all three methods are generated from the **component list**, they stay in sync automatically. Add a component and `equals`, `hashCode`, and `toString` all start including it — eliminating the classic bug where a developer adds a field but forgets to update `equals`/`hashCode`. ## Two important caveats 1. **Floating-point components.** The generated `equals`/`hashCode` use `Double.compare`/`Float.compare`-style semantics, *not* the `==` operator. Consequences: `Double.NaN` **equals** `Double.NaN` (unlike `==`), and `0.0` does **not** equal `-0.0`. 2. **Array components.** Arrays use `Object.equals` (reference identity) and identity hash — the generated methods do **not** compare array *contents*. So two records holding equal-content arrays are considered unequal. If you need content equality, override the methods manually or, better, avoid array components (use an immutable `List`). ## Overriding when needed You *may* override any of the generated methods explicitly (e.g. a custom `toString`), but doing so for `equals`/`hashCode` risks breaking their mutual consistency — do it only with care.

  • A record has a byte[] component. Two instances wrap arrays with identical contents but are reported unequal. Why, and how do you fix it?
    The generated equals uses Object.equals on the array, which is reference identity, so different array instances are never equal even with identical contents (and hashCode is the identity hash). Fix it by overriding equals/hashCode (and ideally toString) to use Arrays.equals/Arrays.hashCode, or — better — store an immutable List<Byte>/defensive copy instead of a raw array so the generated methods compare contents.
  • Can you override just toString on a record while keeping the generated equals and hashCode?
    Yes. You can selectively override any of the three; overriding toString alone is common and harmless. Be cautious overriding equals or hashCode individually, because you must keep them mutually consistent (equal objects must share a hash code).

saying these in an interview costs you the question

  • Saying record equals is reference identity — it is value-based over all components.
  • Assuming array components compare by contents — they compare by reference identity.
  • Thinking you can override equals without also keeping hashCode consistent.
  • Believing NaN behaves with == semantics inside a record — generated equals treats NaN as equal to NaN.
  • Claiming you must hand-write equals/hashCode for records to be valid keys — they are generated and contract-correct already.

context