How do Java records implement equals()/hashCode(), and how would you write a correct manual implementation for a non-record value class?
answer
- Record => equals/hashCode auto-derived from all components, always consistent
- Manual equals: this== ; type/null check ; compare same fields
- Objects.equals for refs, == for primitives, Double/Float.compare for floats, Arrays.equals for arrays
- hashCode = Objects.hash(SAME fields)
- Records are final => no instanceof symmetry trap
basics
~20 sA record auto-generates equals() and hashCode() from all its components, so equal field values mean equal objects. For a regular class, compare each significant field with Objects.equals() in equals(), and combine the same fields with Objects.hash() in hashCode().
solid answer
~50 sRecords (Java 16+) automatically derive equals() and hashCode() from every component: two records are equal iff all their components are equal, and hashCode() combines all components — both consistent by construction, which is why records are the preferred way to write value types. For a non-record class, the canonical equals() does: return true if same reference; return false if the other object isn't the right type (instanceof or getClass()); then compare each equality-significant field, using Objects.equals() for references (null-safe), == for primitives, and Double/Float.compare() for floating-point. hashCode() must combine the exact same fields, typically via Objects.hash(f1, f2, ...). The single most important rule is that both methods use the identical field set; IDEs generate this pair correctly. You also shouldn't include derived or volatile fields, and for performance-sensitive immutable objects you can cache the hash code.
code
java · 22 linesimport java.util.Objects;
// Preferred for a pure value type — nothing to write:
record Money(long amount, String currency) {}
// Manual equivalent for a non-record class:
final class MoneyClass {
private final long amount;
private final String currency;
MoneyClass(long amount, String currency) {
this.amount = amount; this.currency = currency;
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MoneyClass m)) return false;
return amount == m.amount
&& Objects.equals(currency, m.currency); // same fields...
}
@Override public int hashCode() {
return Objects.hash(amount, currency); // ...as hashCode
}
}go deeper
Knows records generate equals/hashCode, and can use Objects.hash()/Objects.equals() with an IDE template for a regular class.
Writes the four-step canonical equals() by hand, uses the right per-type comparison, and ensures hashCode uses the same fields; knows records are final and consistent.
Chooses records vs manual deliberately, handles float/array edge cases, excludes non-identity fields, and considers hash caching for immutable hot-path objects.
Standardizes value-type style across the codebase (records by default, generated methods, explicit field inclusion), and reasons about hashCode distribution/perf and serialization interplay.
## What a record gives you A **record** is a concise, immutable carrier of data: `record Point(int x, int y) {}`. The compiler generates a canonical constructor, accessors, `toString()`, and — most relevant here — **`equals()` and `hashCode()` derived from all components**: - `equals(o)`: true iff `o` is the same record type and *every* component is equal (using `Objects.equals` semantics, `Double.compare`/`Float.compare` for floating point). - `hashCode()`: a combination of all the components' hash codes. Because both are generated from the *same* components, they're **guaranteed consistent** — you cannot accidentally use different fields. Records are also implicitly `final`, so the subclass/`instanceof` symmetry problem can't arise. **This makes records the recommended default for value types** that need equality. ## Writing it by hand (non-record class) The canonical, contract-correct `equals()` has four steps: ```java @Override public boolean equals(Object o) { if (this == o) return true; // 1. self-check (fast path + reflexive) if (!(o instanceof Money m)) return false; // 2. type + null check (null instanceof => false) return amount == m.amount // 3a. primitives with == && Objects.equals(currency, m.currency);// 3b. references null-safe } ``` Field-comparison rules: - **Primitives** (`int`, `long`, `boolean`, `char`): `==`. - **Object references**: `Objects.equals(a, b)` — null-safe; avoids NPE and an explicit null check. - **`float`/`double`**: `Float.compare`/`Double.compare`, *not* `==`, so that `NaN` equals `NaN` and `+0.0`/`-0.0` are distinguished consistently with hashCode. - **Arrays**: `Arrays.equals` (or `Arrays.deepEquals` for nested arrays); plain `Objects.equals` only compares array identity. The matching `hashCode()`: ```java @Override public int hashCode() { return Objects.hash(amount, currency); // SAME fields as equals() } ``` `Objects.hash(...)` boxes its varargs (a small allocation); for a hot path you can hand-roll the classic `31 * result + fieldHash` loop, or compute the hash once and cache it in a `final` field for an immutable object. ## The cardinal rules 1. **Same fields in both methods.** This is the one rule that, if broken, breaks the contract. Cross-check that `equals()` and `hashCode()` reference an identical field list. 2. **Only equality-significant fields.** Exclude caches, lazily-derived values, or fields not part of logical identity. 3. **Don't include a `super.hashCode()`/`super.equals()`** unless the superclass also follows the contract on the relevant fields. 4. **Keep it consistent and pure** — no time, randomness, or I/O. ## Tooling - **IDEs** (IntelliJ/Eclipse) generate the matched pair; choose `instanceof` vs `getClass()` deliberately. - **Lombok `@EqualsAndHashCode`** (with `of`/`exclude`) and **`@Value`** generate them; be explicit about which fields to include. - **Apache Commons** `EqualsBuilder`/`HashCodeBuilder` are an older fluent option (reflection-based variants are slow/fragile — prefer explicit fields). When in doubt for a pure value type: **use a record** and write nothing.
- Why use Double.compare instead of == for double fields in equals()?== treats NaN != NaN (so NaN fields would never be equal even to themselves) and treats +0.0 == -0.0, which is inconsistent with how hashCode() handles them. Double.compare gives the consistent, contract-friendly ordering equals() needs.
- What's the advantage of caching the hash code, and when is it safe?It avoids recomputing the hash on every collection operation. It's safe only for immutable objects (fields never change), commonly via a lazily-initialized final/volatile int field — String does exactly this.
saying these in an interview costs you the question
- Using == on float/double instead of Double.compare (NaN/-0.0 bugs)
- Comparing arrays with Objects.equals instead of Arrays.equals
- equals() and hashCode() built from different field sets
- Adding a value-defining component to something that should've been a record/final
- Using reflection-based equals/hashCode builders in hot paths