Non-record concrete value classes (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:
String, collections, or arrays) is compared using reference equality (==) rather than .equals(), and hashed using System.identityHashCode rather than .hashCode().Rational(1, 2) vs. Rational(2, 4)), or internal fields that should not participate in equality—are directly exposed in equality comparisons.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.If the class is a simple data carrier whose equality is defined by its components, prefer a value record:
value record UserId(String id) {}
If the class requires encapsulation (such as private fields, normalization, or defensive copies), explicitly implement equals and hashCode:
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:
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(); } }