What Methods are Automatically Generated for a Record
When you declare a record, the Java compiler automatically synthesizes 5 core constructs:
🟢 Junior Level
When you declare a record, the Java compiler automatically synthesizes 5 core constructs:
- Canonical Constructor: Accepts all declared components in order and assigns them to corresponding
private finalfields. - Public Component Accessors: Generated for each component with names matching the components directly (e.g.,
user.name(),user.age()— without thegetprefix). equals(Object o): Returnstrueif the compared object is of the same record class and all components are equal.hashCode(): Computes a combined hash code derived from all record components.toString(): Returns a descriptive string representation in the formatRecordName[comp1=val1, comp2=val2].
public record User(String name, int age) {}
// Using all automatically generated methods:
User user1 = new User("Alice", 25); // 1. Canonical Constructor
User user2 = new User("Alice", 25);
System.out.println(user1.name()); // 2. Component Accessor: "Alice"
System.out.println(user1.age()); // 2. Component Accessor: 25
System.out.println(user1.equals(user2)); // 3. Value equality: true
System.out.println(user1.hashCode() == user2.hashCode()); // 4. Hash equality: true
System.out.println(user1); // 5. "User[name=Alice, age=25]"
🟡 Middle Level
1. Mechanics of Generated Equality and Hash Methods
equals(Object o):- Tests reference identity (
this == o). - Validates class equality:
o != null && this.getClass() == o.getClass(). - Primitive fields are compared using
==(forfloat/double,Float.compare()andDouble.compare()are used to handleNaNand-0.0correctly). - Reference components are compared using
Objects.equals(this.field, other.field).
- Tests reference identity (
hashCode():- Combines individual component hashes using prime factor multiplication ($31$), ensuring strict compatibility with the
equals()contract forHashMapandHashSet.
- Combines individual component hashes using prime factor multiplication ($31$), ensuring strict compatibility with the
2. Critical Trap: Records with Array Components (byte[], int[])
[!CAUTION] The auto-generated
equals(),hashCode(), andtoString()methods do not perform deep array comparisons! For an array component,Objects.equals(a, b)performs reference identity comparison (==), not element-by-element equality!
public record BinaryPayload(String id, byte[] data) {}
BinaryPayload p1 = new BinaryPayload("1", new byte[]{1, 2, 3});
BinaryPayload p2 = new BinaryPayload("1", new byte[]{1, 2, 3});
// ❌ Unexpected behavior: distinct heap arrays have different references!
System.out.println(p1.equals(p2)); // FALSE!
System.out.println(p1); // BinaryPayload[id=1, data=[B@6bdf28bb] (prints memory address!)
The Fix: Explicit Override with Arrays
When a record contains an array component, you must explicitly override all three methods:
public record BinaryPayload(String id, byte[] data) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BinaryPayload that)) return false;
return Objects.equals(id, that.id) && Arrays.equals(data, that.data);
}
@Override
public int hashCode() {
int result = Objects.hashCode(id);
return 31 * result + Arrays.hashCode(data);
}
@Override
public String toString() {
return "BinaryPayload[id=" + id + ", data=" + Arrays.toString(data) + "]";
}
}
🔴 Senior Level
Bytecode Architecture: invokedynamic and ObjectMethods.bootstrap
The javac compiler does not generate static boilerplate bytecode for equals(), hashCode(), and toString() inside each compiled .class file.
Instead, it emits an invokedynamic (indy) instruction pointing to the bootstrap method:
// javap -v bytecode output:
public final boolean equals(java.lang.Object);
descriptor: (Ljava/lang/Object;)Z
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
0: aload_0
1: aload_1
2: invokedynamic #17, 0 // BootstrapMethods #0:equals:(LUser;Ljava/lang/Object;)Z
7: ireturn
Bootstrap Method: java.lang.runtime.ObjectMethods.bootstrap(...)
Upon first execution, the JVM resolves the bootstrap method:
public static Object bootstrap(
MethodHandles.Lookup lookup,
String methodName,
TypeDescriptor type,
Class<?> recordClass,
String names,
MethodHandle... getters
)
- The runtime receives an array of
MethodHandlegetters for all components. ObjectMethodsdynamically generates a specialized method handle call chain customized for that specific record type.- The C2 JIT compiler aggressively inlines these call sites directly into machine code.
Architectural Advantages:
- Drastically reduces
.classbytecode footprint. - Allows JDK updates to optimize hashing and equality algorithms transparently without requiring re-compilation of user code.
Why Records Do Not Cache hashCode
java.lang.String caches its hash code in a private mutable field private int hash. Upon first invocation, it is computed and stored for $O(1)$ future access.
In a Record, this optimization is impossible:
- The Java Language Specification strictly forbids non-static instance fields in the record body.
- You cannot declare a hidden
private int cachedHashCode = 0;. - Every call to
record.hashCode()recalculates the hash across all components. If a record contains large nested structures, frequenthashCode()invocations in hot loops can become a CPU bottleneck.
4 Tricky Questions
1. How does the compiler implement equals, hashCode, and toString in bytecode: does it emit static Java instructions or use invokedynamic?
Answer:
The compiler emits an invokedynamic call linked to the bootstrap method java.lang.runtime.ObjectMethods.bootstrap.
There are no static if-else chains or manual arithmetic operations hardcoded in the class bytecode. At class link time, the bootstrap method dynamically constructs an optimized MethodHandle call chain tailored to the record’s components. This minimizes class file size and enables JVM-level optimizations and future algorithmic improvements without recompilation.
2. What happens to auto-generated equals(), hashCode(), and toString() if a component is an array (e.g. byte[] payload)?
Answer: A logical defect occurs:
equals()delegates toObjects.equals(), which evaluates reference identity (==) on arrays. Two distinct record instances with identical array contents returnfalseonequals().hashCode()returns the array’s identity hash code (based on memory location), not the hash of its elements.toString()prints the array type and identity hash (e.g.[B@4f023edb), rather than its readable elements. Developers must manually override all three methods using helper utilities fromjava.util.Arrays.
3. Can you implement lazy caching for hashCode() inside a Record, similar to java.lang.String?
Answer:
No, not using instance fields.
JLS §8.10 strictly forbids declaring any non-static instance fields inside the body of a record. Because all instance fields must be defined in the header, you cannot add a private int cachedHash; field. The hash code is computed afresh on every invocation. If caching is essential for performance, you must either maintain an external cache (e.g., ConcurrentHashMap) or use a traditional final class.
4. Does a Record generate a clone() method or Java Serialization methods (readObject / writeObject)?
Answer: No, it does not.
clone()is neither generated nor supported. Because records are immutable, cloning is redundant (a direct reference copy is safe, and state updates use the Wither pattern).readObjectandwriteObjectare not generated, and declaring them manually is ignored by the JVM. Record serialization is fixed at the specification level: deserialization occurs exclusively through the canonical constructor.
🎯 Interview Cheat Sheet
Summary in 60 Seconds
- 5 Generated Constructs: Canonical constructor, component accessors (
name(), noget),equals(Object),hashCode(), andtoString(). - Bytecode:
equals,hashCode, andtoStringuseinvokedynamicwithObjectMethods.bootstrap. - Array Hazard: Default
equals/hashCodecompare array references, not contents. Explicitly override withArrays.*if arrays are components. - Hash Caching: Impossible inside the record because extra instance fields are prohibited.
- Serialization:
readObject/writeObjectmethods are ignored; deserialization strictly invokes the canonical constructor.
Common Interview Traps
- Forgetting that array components break auto-generated
equals()andhashCode(). - Assuming accessors use the JavaBeans naming standard (
getName()instead ofname()). - Attempting to add a cached hash code field to a Record.