Write a correct, contract-compliant equals() for a value class and explain each step, including the use of instanceof, null handling, and field comparison.
answer
- this == o fast path (reflexive)
- instanceof = null-safe + type + cast in one line
- Double.compare for floats, Objects.equals for objects, Arrays.equals for arrays
- Same fields in hashCode via Objects.hash
- Immutable equality fields; prefer records
basics
~20 sStart with an identity fast-path (this == o), then an instanceof check that also rejects null and wrong types, cast, and compare the significant fields with == for primitives and equals() for objects. Override hashCode over the same fields.
solid answer
~40 sA canonical equals does five things. First, `if (this == o) return true;` — a cheap reflexive fast-path. Second, `if (!(o instanceof MyType x)) return false;` — this single check rejects null (null instanceof is false) and any incompatible type, and the pattern-variable gives you the cast for free, keeping equals symmetric. Third, compare each significant field: primitives with `==`, but use `Float.compare`/`Double.compare` for float/double (to handle NaN and ±0.0 correctly), object fields with `Objects.equals(a,b)` (null-safe), and arrays with `Arrays.equals`. Include only fields that define logical equality — skip derived/cached or irrelevant ones. Fourth, always override `hashCode` over the same fields, e.g. `Objects.hash(...)`. Fifth, keep the fields immutable if the object is used as a map key. Compare cheaper/more-discriminating fields first for performance. Records generate all of this automatically when value semantics fit.
code
java · 22 linespublic final class Money {
private final long amountMinor; // primitive
private final String currency; // object, non-null by construction
public Money(long amountMinor, String currency) {
this.amountMinor = amountMinor;
this.currency = java.util.Objects.requireNonNull(currency);
}
@Override public boolean equals(Object o) {
if (this == o) return true; // reflexive fast path
if (!(o instanceof Money m)) return false; // null + type + cast
return amountMinor == m.amountMinor // primitive: ==
&& currency.equals(m.currency); // object: equals
}
@Override public int hashCode() { // same fields
return java.util.Objects.hash(amountMinor, currency);
}
}
// Or simply: record Money(long amountMinor, String currency) {}go deeper
Can produce the basic shape: this==o, instanceof, compare fields, and remember hashCode.
Explains why instanceof covers null, uses Double.compare/Objects.equals/Arrays.equals correctly, and keeps hashCode in sync.
Discusses field-ordering for short-circuit performance, immutability for keys, instanceof vs getClass trade-offs, and when to ignore fields.
Standardizes value-type modeling org-wide (records/sealed types, generation, EqualsVerifier in tests), and weighs entity-identity vs value-equality semantics in domain design.
## Goal Write `equals` that is reflexive, symmetric, transitive, consistent, null-false — and pairs with a correct `hashCode`. Here is the canonical template, then a line-by-line rationale. ```java public final class Point { private final int x; private final double y; private final String label; // object field (may be null) private final int[] tags; // array field @Override public boolean equals(Object o) { if (this == o) return true; // (1) if (!(o instanceof Point p)) return false; // (2) return x == p.x // (3a) primitive && Double.compare(y, p.y) == 0 // (3b) float/double && java.util.Objects.equals(label, p.label) // (3c) null-safe object && java.util.Arrays.equals(tags, p.tags); // (3d) array } @Override public int hashCode() { // (4) return java.util.Objects.hash( x, y, label, java.util.Arrays.hashCode(tags)); } } ``` ## (1) Identity fast-path: `if (this == o) return true;` `==` here is reference identity. If the argument *is* this object, it's trivially equal — return immediately. This guarantees reflexivity and is a cheap optimization for the common case of comparing an object to itself (e.g. in collection internals). Optional for correctness but standard. ## (2) Type check with `instanceof`: `if (!(o instanceof Point p)) return false;` This one line does three jobs: - **Null handling:** `null instanceof Point` is `false`, so `o == null` returns false here without a separate null check — satisfying the non-nullity clause and avoiding any `NullPointerException`. - **Type safety:** any object that isn't a `Point` is rejected. - **Cast for free:** the *pattern variable* `p` (Java 16+) is the already-cast `Point`, so you don't write `(Point) o` later. **Why `instanceof` and not `getClass()`?** `instanceof` lets a subclass be equal to a superclass instance, which keeps equality usable polymorphically but is what creates the value-adding-subclass hazard discussed elsewhere; `getClass()` is stricter (exact class) but breaks Liskov substitution. For a `final` value class like this, the distinction is moot — no subclasses exist — so `instanceof` is clean and idiomatic. (Records use a `getClass`-style check and are final.) ## (3) Field comparison — pick the right tool per field type Compare **only the fields that define logical equality** (skip caches, derived values, lazily computed fields): - **(3a) Primitives** (`int`, `long`, `char`, `boolean`, `byte`, `short`): use `==`. - **(3b) `float`/`double`:** use `Float.compare`/`Double.compare`, **not** `==`. Reasons: `Double.compare` treats `Double.NaN` as equal to itself (whereas `NaN == NaN` is false) and distinguishes `0.0` from `-0.0` consistently with `hashCode`. Using raw `==` would make an object containing `NaN` fail reflexivity. - **(3c) Object references** (including boxed types, `String`): use `Objects.equals(a, b)` — it is **null-safe** (returns true if both null, false if exactly one is null, else `a.equals(b)`), so you don't hand-write null checks. - **(3d) Arrays:** use `Arrays.equals` (or `Arrays.deepEquals` for nested arrays). A plain array `equals` is identity-based and almost never what you want. ## (4) hashCode over the same fields The equals/hashCode contract requires equal objects to share a hash code, so compute `hashCode` from **exactly the same fields**, in any order, via `Objects.hash(...)` (boxing each field; use `Arrays.hashCode(array)` for array members). If equals uses `Double.compare`, hashCode should use the field's value consistently — `Objects.hash(double)` boxes via `Double.hashCode`, which aligns with `Double.compare`. ## Performance & ordering Within (3), compare the **cheapest and most likely-to-differ** fields first so unequal objects short-circuit early (e.g. an `int id` before a long `String`). Avoid expensive or network-bound comparisons (no DNS, no DB) to keep equals consistent and fast. ## Immutability for keys If instances are used as keys in a `HashMap`/`HashSet`, the equals/hashCode fields should be **immutable** (`final`, no setters). Mutating an equality field after insertion strands the entry in the wrong bucket. ## Just use a record when you can For a pure value type, `record Point(int x, double y, String label)` auto-generates a correct `equals`, `hashCode`, and `toString` over the components (with the float/double and null handling done right), and the class is implicitly `final`. Hand-write `equals` only when you need custom semantics (e.g. ignore a field, normalize before comparing) or can't use a record. ## Takeaway Fast-path identity → `instanceof` (null + type + cast) → field-by-field with the right comparator per type → matching `hashCode`. Keep equality fields immutable, order comparisons cheaply, and prefer records when the default semantics fit.
- Why use Double.compare(y, p.y) == 0 instead of y == p.y for a double field?Because == treats NaN as not equal to itself (breaking reflexivity for an object holding NaN) and conflates +0.0 with -0.0 inconsistently with hashCode. Double.compare makes NaN equal to NaN and orders ±0.0 consistently, keeping the contract.
- When would you deliberately exclude a field from equals/hashCode?For derived/cached values (recomputable from other fields), for identity-only metadata irrelevant to logical equality (e.g. a creation timestamp on a value object), or for mutable fields you don't want to participate so the object can stay a stable map key.
It's like checking ID at a door: first 'is this literally me?' (fast path), then 'are you even a member of this club?' (instanceof rejects null/outsiders), then compare each credential field with the right kind of check.
saying these in an interview costs you the question
- Casting with (MyType) o before/without an instanceof or getClass check (risks ClassCastException and breaks null-false).
- Using == on float/double fields.
- Using == or plain .equals on arrays instead of Arrays.equals.
- Overriding equals but forgetting hashCode, or using different fields in each.
- Doing expensive/non-deterministic work in equals (breaks consistency).