blob: 777f2340ced04a5a31b20bcc2313e3d52b5ee7bc [file] [view] [edit]
Non-record concrete `value` classes ([JEP 401][jep-401]) should explicitly
override `equals` and `hashCode`.
Unlike a `value record`—which automatically synthesizes component-wise `equals`
and `hashCode` methods that call `.equals()` and `.hashCode()` on reference
components—a non-record `value class` that omits `equals` and `hashCode`
inherits `Object.equals` (`return this == obj;`) and `Object.hashCode`
(`System.identityHashCode(this)`).
For a `value class`, `Object.equals` and `this == obj` perform a state-wise
identity comparison of the class's fields:
* **Reference fields are compared by identity**: Any reference field (such as
`String`, collections, or arrays) is compared using reference equality
(`==`) rather than `.equals()`, and hashed using `System.identityHashCode`
rather than `.hashCode()`.
* **Representation leakage**: Any private representation details—such as
defensive array copies, unnormalized state (e.g., `Rational(1, 2)` vs.
`Rational(2, 4)`), or internal fields that should not participate in
equality—are directly exposed in equality comparisons.
* **Evolution hazard**: Even if a `value class` initially contains only
primitive or `enum` fields where `Object.equals` happens to match logical
equality, omitting `equals` and `hashCode` creates a hazard when the class
evolves: adding a reference field (such as `String`) or an auxiliary field
later will silently break `.equals()` and `.hashCode()`. Requiring an
explicit definition upfront ensures equality semantics remain intentional as
fields change.
### How to fix
If the class is a simple data carrier whose equality is defined by its
components, prefer a `value record`:
```java
value record UserId(String id) {}
```
If the class requires encapsulation (such as private fields, normalization, or
defensive copies), explicitly implement `equals` and `hashCode`:
```java
value class UserId {
private final String id;
UserId(String id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
return obj instanceof UserId other && Objects.equals(id, other.id);
}
@Override
public int hashCode() {
return Objects.hashCode(id);
}
}
```
If all fields of the `value class` are primitives or enums and state-wise
comparison via `Object.equals` / `Object.hashCode` is genuinely intended,
delegate explicitly so that the choice is clear to readers:
```java
value class Meters {
private final int value;
Meters(int value) {
this.value = value;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
@Override
public int hashCode() {
return super.hashCode();
}
}
```
[jep-401]: https://openjdk.org/jeps/401