📋 Section 20 · Question #5

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:

  1. Canonical Constructor: Accepts all declared components in order and assigns them to corresponding private final fields.
  2. Public Component Accessors: Generated for each component with names matching the components directly (e.g., user.name(), user.age() — without the get prefix).
  3. equals(Object o): Returns true if the compared object is of the same record class and all components are equal.
  4. hashCode(): Computes a combined hash code derived from all record components.
  5. toString(): Returns a descriptive string representation in the format RecordName[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 == (for float/double, Float.compare() and Double.compare() are used to handle NaN and -0.0 correctly).
    • Reference components are compared using Objects.equals(this.field, other.field).
  • hashCode():
    • Combines individual component hashes using prime factor multiplication ($31$), ensuring strict compatibility with the equals() contract for HashMap and HashSet.

2. Critical Trap: Records with Array Components (byte[], int[])

[!CAUTION] The auto-generated equals(), hashCode(), and toString() 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
)
  1. The runtime receives an array of MethodHandle getters for all components.
  2. ObjectMethods dynamically generates a specialized method handle call chain customized for that specific record type.
  3. The C2 JIT compiler aggressively inlines these call sites directly into machine code.

Architectural Advantages:

  • Drastically reduces .class bytecode 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, frequent hashCode() 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 to Objects.equals(), which evaluates reference identity (==) on arrays. Two distinct record instances with identical array contents return false on equals().
  • 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 from java.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).
  • readObject and writeObject are 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(), no get), equals(Object), hashCode(), and toString().
  • Bytecode: equals, hashCode, and toString use invokedynamic with ObjectMethods.bootstrap.
  • Array Hazard: Default equals/hashCode compare array references, not contents. Explicitly override with Arrays.* if arrays are components.
  • Hash Caching: Impossible inside the record because extra instance fields are prohibited.
  • Serialization: readObject/writeObject methods are ignored; deserialization strictly invokes the canonical constructor.

Common Interview Traps

  • Forgetting that array components break auto-generated equals() and hashCode().
  • Assuming accessors use the JavaBeans naming standard (getName() instead of name()).
  • Attempting to add a cached hash code field to a Record.