skip to content

Given Java records and IDE/Lombok generators, when should you still hand-write equals() and hashCode(), and what governs that decision?

level: principalimportance: nice to knowfreq 35%

answer

  1. default: record or generator, not hand-written
  2. override for subset/business-key equality
  3. override for normalization (case, BigDecimal scale)
  4. records compare array components by identity → override
  5. JPA entities: mutable id + proxies break generated equals

basics

~20 s

Most of the time you should not hand-write them: use a record or a generator, which produce correct equals/hashCode from your fields. Hand-write only when you need custom equality, such as comparing a subset of fields, normalizing values, or handling array fields that records compare by identity.

solid answer

~50 s

The default in modern Java is to not author equals/hashCode by hand: records generate them from all components, and IDE/Lombok generators produce the standard skeleton. You reach for a manual implementation only when the generated semantics are wrong for the domain. Common triggers: equality should use a subset of fields (e.g. an entity keyed by id only); values must be normalized before comparison (trim/lowercase, scale a BigDecimal); a field needs custom comparison the generator does not provide; or there are array components, which records and naive generators compare by identity rather than content. You also decide identity-vs-value semantics: JPA entities are notoriously tricky because mutable ids and lazy proxies make generated equals/hashCode unstable across the persistence lifecycle, so teams often use a business key or a stable surrogate. The governing principles are the equals contract (reflexive, symmetric, transitive, consistent, non-null) and the equals/hashCode consistency rule; whatever you write or generate must honour them, and equals and hashCode must use the same field set.

go deeper

for a junior

Knows records and IDE generators produce equals/hashCode so you usually don't write them by hand.

for a middle

Can name concrete reasons to override (subset of fields, normalization, array fields) and that the same field set must back both methods.

for a senior

Explains record array-component identity, BigDecimal scale, and the basic JPA id pitfalls, and validates against the equals contract.

for a principal

Sets the team policy (record vs generator vs hand-written + EqualsVerifier tests), resolves JPA entity identity strategy (business key / client UUID), and reasons about contract preservation and hash-key immutability across the domain model.

## The modern baseline: don't write them yourself Hand-rolling equals/hashCode is error-prone, so prefer generation: - **`record`** (Java 16+): the compiler generates `equals`, `hashCode`, and `toString` from the record components, using value semantics (`Double.compare`, `Objects.equals`, etc.). For a pure immutable value carrier, a record is the right answer and removes the boilerplate entirely. - **IDE generators** (IntelliJ/Eclipse) and **Lombok `@EqualsAndHashCode`/`@Value`** produce the standard skeleton for classes that cannot be records (mutable, extends a class, framework-managed). These cover the large majority of cases correctly. The principal-level skill is knowing the **exceptions**, because that is where generated code is silently wrong. ## When to override / hand-write anyway 1. **Subset-of-fields (business key) equality.** An entity may be equal by `id` alone, not all fields. Records compare *all* components, so you either don't use a record or override the generated methods. Example: two `User` rows are the same user if `id` matches, regardless of `lastLoginAt`. 2. **Normalization before comparison.** Equality should ignore case, trimmed whitespace, or `BigDecimal` scale (`new BigDecimal("1.0").equals(new BigDecimal("1.00"))` is false). You normalize in a compact constructor (record) or in the field comparison. 3. **Array components.** Records and naive generators compare arrays by **identity**. If a value object holds an array you must override using `Arrays.equals`/`Arrays.hashCode` (or `deepEquals`/`deepHashCode`), or avoid array fields (prefer `List`). 4. **Custom field comparison.** A field whose own equals is unsuitable (e.g. a type compared by reference but logically equal by content). 5. **Performance on a hot path.** Replacing the varargs `Objects.hash` allocation with a manual loop, or caching the hash for an immutable type. ## JPA entities — the canonical hard case Generated equals/hashCode on `@Entity` classes is a known trap: - Including the auto-generated `id` makes hashCode **change** when the entity is persisted (id goes null → assigned), so an entity put in a `HashSet` before flush becomes unfindable after. - Including mutable business fields makes equality drift as the object is edited. - Lazy-loading **proxies** mean `getClass()` returns a proxy subclass, breaking exact-class checks (use `instanceof`/`Hibernate.getClass`). - Common resolutions: a stable **business key**, a **client-assigned UUID** id set at construction, or a constant hashCode plus id-based equals (accepting bucket degradation). There is no one-size answer — it is a design decision. ## The governing rules (what any solution must satisfy) Whatever you generate or write must obey: - The **equals contract**: reflexive, symmetric, transitive, consistent, and `x.equals(null) == false`. - The **equals/hashCode consistency rule**: equal objects must have equal hash codes; both must use the **same field set**. - **Immutability of equality fields** for hash keys: never include a mutable field in hashCode if the object will be a hash key (the entry is lost when the field mutates). - **getClass vs instanceof** chosen deliberately for the hierarchy (subclasses equal or not). ## Decision heuristic Pure immutable value with no arrays → **record**. Class needing the standard all-fields semantics → **generator**. Anything with subset/normalized/array/identity-vs-value subtlety, or a persistence lifecycle → **hand-write deliberately**, and test the contract (e.g. with EqualsVerifier).

  • Why are JPA entity equals/hashCode considered a special hazard?
    Auto-generated ids are null before persist and assigned after, so id-based hashCode changes mid-lifecycle and breaks HashSet membership; mutable business fields make equality drift; and lazy proxies are subclasses, breaking getClass checks. Teams use a stable business key, a client-assigned UUID, or accept a constant hashCode with id-based equals.
  • A record holds a BigDecimal. Any equals concern?
    Yes — BigDecimal.equals considers scale, so new BigDecimal("1.0") is not equal to new BigDecimal("1.00"). If the domain treats them as equal you must normalize (e.g. stripTrailingZeros in the compact constructor) or override equals/hashCode; the generated record methods use BigDecimal.equals as-is.

saying these in an interview costs you the question

  • Hand-writing equals/hashCode when a record would be correct, then introducing bugs
  • Using a record with an array component and assuming content equality
  • Putting a generated/mutable JPA id in hashCode so the entity is lost across a flush
  • Including all fields in entity equality when business identity is a subset
  • Relying on getClass() equality for Hibernate-proxied entities

context