equals / hashCode / toString — Specification Reading Guide¶
Unlike SOLID, the equality contract is normative. The five clauses, the agreement with
hashCode, and the recommendedtoStringformat all live in the Javadoc ofjava.lang.Object— and the JDK collection framework assumes you respect them. This file maps each clause to the binding spec text, walks theObjectsutility class (java.util.Objects), and covers the modern Java features (JEP 395 records, JEP 394 pattern-matching instanceof, JEP 409 sealed types) that change how you write the contract today.
1. Where to find the canonical text¶
| Concept | Authoritative source |
|---|---|
Object.equals(Object) — the five clauses | java.lang.Object Javadoc, equals method |
Object.hashCode() — the agreement rule | java.lang.Object Javadoc, hashCode method |
Object.toString() — the convention | java.lang.Object Javadoc, toString method |
java.util.Objects utility methods | java.util.Objects Javadoc (Java 7+) |
| Records — auto-generated contracts | JLS §8.10, JEP 395 |
Pattern matching for instanceof | JLS §14.30.1, JEP 394 |
| Sealed classes — closed equality hierarchies | JLS §8.1.1.2, JEP 409 |
String.hashCode() — the canonical algorithm | String Javadoc, hashCode method |
Comparable.compareTo consistency note | java.lang.Comparable Javadoc |
instanceof null behaviour | JLS §15.20.2 — "is null" always false |
Bytecode for instanceof | JVMS §6.5.instanceof, §3.10 (verifier rules) |
invokevirtual for equals/hashCode | JVMS §6.5.invokevirtual |
Identity hash code (System.identityHashCode) | System Javadoc; HotSpot mark-word layout |
The Javadoc of Object is the spec for this whole topic. Read it once per career — it is shorter than this file and authoritative.
2. The five clauses verbatim — Object.equals(Object)¶
The Javadoc text (from JDK 21, unchanged in meaningful ways since Java 1.0):
The
equalsmethod implements an equivalence relation on non-null object references: - It is reflexive: for any non-null reference valuex,x.equals(x)should returntrue. - It is symmetric: for any non-null reference valuesxandy,x.equals(y)should returntrueif and only ify.equals(x)returnstrue. - It is transitive: for any non-null reference valuesx,y, andz, ifx.equals(y)returnstrueandy.equals(z)returnstrue, thenx.equals(z)should returntrue. - It is consistent: for any non-null reference valuesxandy, multiple invocations ofx.equals(y)consistently returntrueor consistently returnfalse, provided no information used inequalscomparisons on the objects is modified. - For any non-null reference valuex,x.equals(null)should returnfalse.
Every word is doing work:
- "equivalence relation" is the mathematical noun: a binary relation that is reflexive, symmetric, and transitive. The Javadoc adds consistency and non-null on top to handle Java's reference and mutability semantics.
- "should return" — not "must". The Javadoc is permissive in tone; in practice, the JDK collection framework treats these as must, and breaking them produces silent collection corruption rather than
IllegalStateException. - "non-null reference values" — the contract is defined over non-null references. The fifth clause covers null explicitly.
- "provided no information used in
equalscomparisons on the objects is modified" — this is the consistency carve-out that permits mutable fields inequalsif you also promise not to mutate them after putting the object in a collection. In practice, this is a contract you cannot enforce, so the senior rule is "don't put mutable fields inequals".
The default implementation, from the same Javadoc:
The
equalsmethod for classObjectimplements the most discriminating possible equivalence relation on objects; that is, for any non-null reference valuesxandy, this method returnstrueif and only ifxandyrefer to the same object (x == yhas the valuetrue).
This is identity equality. It satisfies all five clauses vacuously: a reference is equal only to itself.
3. The hashCode contract verbatim — Object.hashCode()¶
The Javadoc text:
The general contract of
hashCodeis: - Whenever it is invoked on the same object more than once during an execution of a Java application, thehashCodemethod must consistently return the same integer, provided no information used inequalscomparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application. - If two objects are equal according to theequals(Object)method, then calling thehashCodemethod on each of the two objects must produce the same integer result. - It is not required that if two objects are unequal according to theequals(Object)method, then calling thehashCodemethod on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
Two consequences:
- Hashes may change across JVM runs. A hash code from yesterday's process is not portable to today's. Don't serialise hash codes; don't store them in databases; don't expect a constant value.
- Equal objects must hash the same; unequal objects may collide. The asymmetry of the rule is the entire reason
HashMapworks — collisions are expected, the bucket logic walks them.
The default implementation:
As far as is reasonably practical, the
hashCodemethod defined by classObjectreturns distinct integers for distinct objects. This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language.
In HotSpot, the default hashCode is computed once on first use, stashed in the object header's mark word, and reused on subsequent calls. The bit-layout is documented in markWord.hpp; the algorithm has been Marsaglia's xor-shift for a long time. The takeaway: the default hashCode is identity-based and stable for the object's lifetime, but it is not the field-based hash you need for value classes.
System.identityHashCode(Object) returns exactly this default-implementation value, even for objects that have overridden hashCode. It is what IdentityHashMap keys on.
4. The toString convention — Object.toString()¶
The Javadoc text:
Returns a string representation of the object. In general, the
toStringmethod returns a string that "textually represents" this object. The result should be a concise but informative representation that is easy for a person to read. It is recommended that all subclasses override this method.
This is recommendation, not contract. The default implementation:
Customer@7b3f8a2c. Unhelpful in logs. The Javadoc recommends overriding for every class. The team policy in ./professional.md section 8 expands the convention to include redaction rules — none of which the spec mandates.
5. java.util.Objects — the contract-friendly toolkit¶
java.util.Objects was added in Java 7 (JEP 142 work) and expanded since. The methods that matter for equality contracts:
// Null-safe equals — handles null on either side, returns false if exactly one is null.
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
// Null-safe hashCode — returns 0 if the argument is null.
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
// Combine multiple field hashes — uses Arrays.hashCode internally.
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
// Null-safe toString with a default.
public static String toString(Object o, String nullDefault) {
return o != null ? o.toString() : nullDefault;
}
// Null guard — throws NullPointerException with an optional message.
public static <T> T requireNonNull(T obj, String message) {
if (obj == null) throw new NullPointerException(message);
return obj;
}
Two design notes:
Objects.hashallocates anObject[]for the varargs call and boxes primitives. For a single field, preferObjects.hashCode(field). For records, the compiler generates code that avoids the varargs allocation — that is one of the reasons records can be faster than IDE-generatedhashCodein tight loops. See ./optimize.md.Objects.equalsopens with(a == b)— covers two cases at once: both null, or same reference. After that, it delegates toa.equals(b)only whenais non-null.
The class is final and has a private constructor — it is a utility class in the JDK sense. Static-import its methods (import static java.util.Objects.*) only if the team agrees; otherwise the Objects. prefix is the clearer style.
6. Records — JLS §8.10 and JEP 395¶
JLS §8.10 defines record classes. The key normative points:
- A record is implicitly
final(§8.10.1). It cannot be extended. - A record's components are implicitly
finalprivate fields (§8.10.3) plus a public accessor method for each. - The compiler generates a canonical constructor matching the components.
- The compiler generates
equals(Object),hashCode(), andtoString()based on all components (§8.10.3) unless you provide your own override.
The generated equals is approximately:
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MyRecord)) return false;
MyRecord that = (MyRecord) o;
return Objects.equals(this.field1, that.field1)
&& Objects.equals(this.field2, that.field2)
&& /* ... */;
// For primitive fields, `==` is used directly (Float/Double use Float.compare).
}
Generated hashCode:
public int hashCode() {
int h = 0;
h = 31 * h + Objects.hashCode(field1);
h = 31 * h + Objects.hashCode(field2);
// For primitives, the boxed hash is computed directly.
return h;
}
(The exact bytecode uses invokedynamic with a java.lang.runtime.ObjectMethods.bootstrap call site, which generates an efficient implementation at link time. JEP 395 specifies the strategy without specifying the bytecode.)
Generated toString:
You may override any of the three. If you override equals, you should also override hashCode to stay consistent — and the contract clauses still apply, you have just opted out of the auto-generated version.
The JLS prohibits explicit declaration of instance fields (§8.10.3) — a record's state is exactly its components. This is the structural fact that makes the auto-generated equals correct: there are no hidden fields to forget.
7. Pattern matching for instanceof — JEP 394 and JLS §14.30.1¶
The pattern variable form (final in Java 16):
JLS §14.30.1 specifies the pattern matching form of instanceof. Semantically equivalent to:
But shorter, type-checked, and the binding m is definitely assigned only inside the true branch — the compiler refuses code that uses m in the false branch.
The bytecode-level instruction is still instanceof (JVMS §6.5.instanceof), which returns 0 or 1. The pattern-variable form emits the same instanceof plus a checkcast (which the verifier proves redundant) into the true branch. The runtime cost is the same as the classical form; the source-level safety is higher.
JLS §15.20.2 — Type Comparison Operator instanceof — defines the null case: null instanceof T is always false, for every reference type T. This is what makes if (!(o instanceof Money m)) return false; a null-safe one-liner. The earlier defensive pattern (if (o == null) return false; if (!(o instanceof Money)) return false;) is redundant because of this rule.
8. Sealed types — JLS §8.1.1.2 and JEP 409¶
Sealed classes (final in Java 17) close a type hierarchy at compile time:
public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double r) implements Shape {}
public record Square(double s) implements Shape {}
public record Triangle(double b, double h) implements Shape {}
The JLS §8.1.1.2 / §9.1.1.4 rules:
- A
sealedtype declares its permitted direct subtypes withpermits .... - Each permitted subtype must be
final,sealed(with its ownpermits), ornon-sealed. - The permitted set is checked by
javacand recorded in the class file as aPermittedSubclassesattribute. - A pattern-match
switchover a sealed type is exhaustive: omitting any permitted subtype is a compile error.
For equality, the combination of records implementing a sealed interface produces a hierarchy where:
- Each leaf is
final— no inheritance-based equality bugs possible. - Components are
final— no mutable-equality bugs possible. - The compiler-generated
equalsper leaf is strict: aCircleis never equal to aSquare, even with matching radius/side. - The
permitsclause documents the full set of equality cases that will ever exist.
This idiom — sealed interface over records — is the modern equivalent of the senior-level instanceof vs getClass() debate. It dissolves both sides: each record's equals uses instanceof (vacuously, because the class is final and only one runtime type satisfies it), and the parent type cannot have its own equals body to make a cross-class comparison.
9. String.hashCode() — the canonical algorithm¶
String.hashCode is specified by the JDK Javadoc as:
Returns a hash code for this string. The hash code for a
Stringobject is computed ass[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]using
intarithmetic, wheres[i]is theith character of the string,nis the length of the string, and^indicates exponentiation. (The hash value of the empty string is zero.)
This is the exact formula. The "Bloch recipe" of result = 31 * result + field.hashCode() is the same arithmetic generalised to multiple fields. The constant 31 was chosen for arithmetic-encoding reasons (31 * x == (x << 5) - x) and because it is prime; the JDK has held to it for String.hashCode since Java 1.0, and Objects.hash uses the same multiplier internally.
Modern HotSpot intrinsifies String.hashCode with a SIMD-vectorised loop on x86-64 and AArch64; the formula is the spec, the implementation is much faster than a naive loop suggests. For your own hash codes via Objects.hash, the JIT will not vectorise but will inline the call.
10. Comparable consistency — java.lang.Comparable Javadoc¶
The Comparable Javadoc adds a strong recommendation (not a requirement):
It is strongly recommended (though not required) that natural orderings be consistent with
equals. This is so because sorted sets (and sorted maps) without explicit comparators behave "strangely" when they are used with elements (or keys) whose natural ordering is inconsistent withequals. In particular, such a sorted set (or sorted map) violates the general contract for set (or map), which is defined in terms of theequalsmethod.
A class is consistent with equals if and only if e1.compareTo(e2) == 0 has the same boolean value as e1.equals(e2). The classic inconsistent type is BigDecimal:
new BigDecimal("1.0").equals(new BigDecimal("1.00")); // false
new BigDecimal("1.0").compareTo(new BigDecimal("1.00")); // 0
HashMap uses equals; TreeMap uses compareTo. The same key behaves differently in the two collections. The Javadoc warns about this by name in the BigDecimal class documentation. For your own Comparable types, keep them consistent or accept the consequences.
11. JEP references and equality¶
| JEP | Feature | What it changes for equality contracts |
|---|---|---|
| JEP 395 | Records (final in Java 16) | Auto-generates equals/hashCode/toString from components. |
| JEP 394 | Pattern matching for instanceof (final, Java 16) | o instanceof Money m form for null-safe, typed equality bodies. |
| JEP 397, 409 | Sealed classes (preview → final in Java 17) | Closed hierarchies; combined with records, removes inheritance/equality clash. |
| JEP 406, 420, 427, 441 | Pattern matching for switch (preview → final) | Exhaustive switch over sealed types — every equality case checked. |
| JEP 286 | Local-variable type inference (var) | Encourages naming the type in the declaration; the equals body unaffected. |
| JEP 261 | Java Platform Module System | Equality across module boundaries; ServiceLoader cross-classloader implications. |
| JEP 401 (preview) | Value classes | Future: identity-free value types — equality is only by content, no headers, no proxy issues. |
The trajectory is clear: each Java version moves equality contracts further into the type system. Records (16) handle the recipe; pattern instanceof (16) handles the type guard; sealed types (17) close the hierarchy; pattern switch (21) makes exhaustive dispatch over equality cases ergonomic; value classes (Valhalla, future) will collapse the identity-vs-content distinction.
12. The bytecode behind instanceof and equals¶
The instanceof instruction (JVMS §6.5.instanceof):
aload_1 ; load reference on stack
instanceof #2 ; #2 is the constant pool index of class Money
ifeq L_false ; if the result is 0, jump to false branch
The verifier proves that the operand stack at every program point has compatible types. After a successful instanceof check followed by a checkcast, the verifier accepts a Money reference in the slot that previously held an Object.
The pattern-variable form if (o instanceof Money m) { ... m.amount() ... } compiles to:
aload_1
instanceof #2
ifeq L_false
aload_1
checkcast #2 ; checkcast is redundant — verifier could prove it, but javac emits it
astore_2 ; store as `m`
; ... use `m` ...
Modern javac (Java 21+) may elide the checkcast when the verifier accepts the proof; older versions emit it always. Either way, the runtime cost is one instanceof plus at most one checkcast per pattern match — both extremely cheap, both inlined by C2.
invokevirtual (JVMS §6.5.invokevirtual) is what invokes equals and hashCode on a reference. It looks up the method on the runtime class via the vtable. Because Object.equals and Object.hashCode are inherited, every call to them on every object goes through invokevirtual — and is therefore polymorphic. This is what makes set.contains(x) find the right equals body even when the set holds Object references.
13. Reading list¶
java.lang.ObjectJavadoc — the spec forequals,hashCode,toString. Read it once; reread when in doubt.java.util.ObjectsJavadoc — the toolkit for null-safe contracts.- JLS §8.10 — records.
- JLS §14.30.1 — pattern matching for
instanceof. - JLS §8.1.1.2 / §9.1.1.4 — sealed classes and interfaces.
- JEP 395 — records, including the rationale for
equals/hashCodeauto-generation. - JEP 394 — pattern matching for
instanceof. - JEP 409 — sealed classes (final).
- Joshua Bloch — Effective Java, 3rd ed. — Items 10 (
equals), 11 (hashCode), 12 (toString), 14 (compareTo). The canonical treatment. - Jan Ouwens — EqualsVerifier documentation — every clause covered with examples of how to fail it.
ComparableJavadoc — the consistent withequalsnote that bitesBigDecimalusers.String.hashCode()Javadoc — the canonical hash algorithm.
The spec sections do not teach equality — they give you the language to say "this code violates the symmetric clause of Object.equals's contract" rather than "this code is wrong". When a coworker pushes back, you cite the Javadoc paragraph. When a reviewer asks "why does this matter?", you point at the JDK collection class that assumes the contract. The contract is judgement; the spec gives you the levers.