📋 Section 20 · Question #2

What Are the Main Differences Between Record and Regular Class

A record is a specialized class type in Java created exclusively for the role of a transparent, immutable data carrier (Data Carrier / Value Object).


🟢 Junior Level

A record is a specialized class type in Java created exclusively for the role of a transparent, immutable data carrier (Data Carrier / Value Object).

A regular class is a general-purpose object-oriented construct intended to encapsulate mutable state, polymorphic behavior, and business logic.

Comparison of Key Differences

Characteristic record Regular class
Primary Purpose Immutable data carrier General-purpose entity (state + behavior)
Superclass Strictly java.lang.Record java.lang.Object (or any custom class)
Inheritance Prohibited (implicitly final) Allowed (unless explicitly marked final)
Instance Fields Only private final, strictly from header Arbitrary (public, protected, private, mutable)
Accessors (Getters) Auto-generated: user.name() Written manually or generated: user.getName()
equals(), hashCode(), toString() Auto-generated value-based implementations Require manual implementation or Lombok
Declaration Size 1 concise line of code 30–50 lines of boilerplate
// Record: concise, safe, immutable
public record Point(int x, int y) {}

// A regular class with equivalent semantics requires extensive boilerplate:
public final class PointClass {
    private final int x;
    private final int y;

    public PointClass(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override public boolean equals(Object o) { ... }
    @Override public int hashCode() { ... }
    @Override public String toString() { ... }
}

🟡 Middle Level

1. Semantic Contract: “State is the Header”

In a regular class, the relationship between fields, constructor parameters, and getters is merely an informal developer convention. A field might be called x, the constructor parameter initialX, and the getter might return this.x * 2.

In a Record, a fundamental language invariant applies: the state of the object is defined strictly and completely by its header.

  • You cannot add hidden instance state:
    public record User(String name) {
        // private int age; // ❌ Compilation error: non-static field not allowed in record
        public static int counter; // ✅ Static fields are permitted
    }
    
  • The component list in the header (String name, int age) unambiguously defines:
    1. The names, types, and declaration order of private final fields.
    2. The signature of the canonical constructor.
    3. The names and return types of accessor methods (name(), age()).
    4. The exact components used in equals() and hashCode().
    5. The structural format of toString().

2. Constructors: Canonical, Compact, and Secondary

In a regular class, constructors may initialize fields arbitrarily or leave them uninitialized. In a Record:

  1. Canonical Constructor: Generated automatically. If explicitly overridden, its parameters must match the record header components exactly in name and type.
  2. Compact Constructor: Syntax unique to records that eliminates the parameter list, designed for validation and normalization:
    public record Range(int start, int end) {
        public Range {
            if (start > end) {
                throw new IllegalArgumentException("start cannot be greater than end");
            }
        }
    }
    
  3. Secondary Constructors: Must explicitly delegate to the canonical constructor via this(...) as their very first statement:
    public record User(String name, int age) {
        public User(String name) {
            this(name, 18); // Mandatory delegation
        }
    }
    

3. Records and JPA / Hibernate

Can Records replace regular entity classes in JPA?

[!IMPORTANT] Records CANNOT be used as JPA @Entity classes. The Jakarta Persistence specification requires:

  1. A public or protected no-arg constructor (which records do not provide).
  2. Mutable fields or setter methods for dirty-checking and state change tracking.
  3. Ability to create runtime proxies (via ByteBuddy/CGLIB), requiring non-final classes and methods (records are implicitly final).

Where Records excel alongside JPA:

  • DTO projections in JPQL: SELECT new com.example.UserDto(u.id, u.name) FROM User u.
  • Spring Data repository projections (Optional<UserDto> findUserById(Long id)).
  • Composite primary keys (@Embeddable / @EmbeddedId in Hibernate 6+).

🔴 Senior Level

Bytecode, JVM Metadata, and Pattern Matching

At the JVM platform level, a Record is not just compiler syntactic sugar (unlike Kotlin data classes or Lombok annotations). It is a first-class language and bytecode construct:

  1. Bytecode Record Attribute: The javac compiler emits a Record attribute containing an array of RecordComponent entries into the .class file. Frameworks (Jackson, Spring, Hibernate) inspect this metadata directly via reflection without heuristic guesswork.
  2. Structural Deconstruction (Record Patterns in Java 21+, JEP 440): Because the state header transparently defines the object, Java 21 supports pattern matching deconstruction:
    if (obj instanceof Point(int x, int y)) {
        System.out.println("Coordinates: " + x + ", " + y);
    }
    
    String describe(Object shape) {
        return switch (shape) {
            case Circle(double r) -> "Circle with radius " + r;
            case Rectangle(double w, double h) when w == h -> "Square: " + w;
            case Rectangle(double w, double h) -> "Rectangle " + w + "x" + h;
            default -> "Unknown";
        };
    }
    

    Regular classes cannot support structural deconstruction because the JVM cannot guarantee that public methods correspond directly to the internal state.

Serialization Security

In standard Java classes, deserialization (ObjectInputStream) bypasses all constructors and injects fields directly via native Unsafe memory calls, opening attack vectors for corrupted objects.

In Record, deserialization was redesigned from the ground up:

  • Deserialization always invokes the canonical constructor.
  • All validation rules and defensive copies declared in the compact constructor execute unconditionally during deserialization.
  • Serialization attacks via corrupted byte streams cannot bypass record invariants.

Reflection Immutability

In ordinary classes, private final fields can be mutated via reflection (field.setAccessible(true); field.set(...)). For Record components, Field.set() at the JVM level explicitly checks:

if (declaringClass.isRecord()) {
    throw new IllegalAccessException("Can not set final field of a record class");
}

Record fields are physically immutable against reflection modification.


4 Tricky Questions

1. Why are instance fields forbidden in the body of a Record, while static fields are permitted?

Answer: Because the foundational architectural invariant of a Record is: “The state of the record is defined entirely by its header”. If arbitrary instance fields were permitted in the body { ... }:

  1. Auto-generated equals(), hashCode(), and toString() would fail to account for the full object state.
  2. Pattern Matching deconstruction in instanceof and switch could not reliably reconstruct or deconstruct the instance.
  3. Secure deserialization through the canonical constructor would be impossible. Static fields are allowed because they reside in class metadata (Metaspace), belong to the class itself, and do not contribute to instance state.

2. Can a Record be used with JPA / Hibernate, and in what scenarios?

Answer: A Record cannot be used as a JPA @Entity. JPA entities require a default no-arg constructor, field mutability for dirty checking, and non-final classes for proxy generation (CGLIB/ByteBuddy). However, Records are ideal for:

  1. DTO Query Projections: Constructing lightweight read-only results in JPQL (SELECT new com.app.UserSummary(u.id, u.name) FROM User u).
  2. Spring Data Projections: Repository query return types (Optional<UserDto> findUserById(Long id)).
  3. Composite Primary Keys (@Embeddable / @EmbeddedId): Supported in Hibernate 6+, since records naturally satisfy immutability, equals(), and hashCode() requirements.

3. Why are Record accessor methods named field() rather than following the JavaBeans getField() convention?

Answer: The Java language designers intentionally stepped away from the 1996 JavaBeans specification. A Record is not a mutable JavaBean with property change events; it is a mathematical nominal tuple. Omitting the get prefix:

  1. Emphasizes that the method is a pure projection of the component value.
  2. Harmonizes Java with modern languages (Kotlin, Scala, C#) that use direct component names.
  3. Eliminates awkward naming edge cases such as getIsActive() vs isActive().

4. How do Records integrate with Pattern Matching in Java 21 (Record Patterns)?

Answer: Java 21 (JEP 440) introduces Record Patterns, enabling nested structural deconstruction. Because the JVM guarantees that the components declared in the header represent the complete state, the compiler allows unpacking components directly:

record Point(int x, int y) {}
record Line(Point start, Point end) {}

if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
    int dx = x2 - x1;
    int dy = y2 - y1;
}

Regular classes cannot participate in this syntax because the compiler cannot verify that arbitrary getter methods correspond to constructor parameters.


🎯 Interview Cheat Sheet

Summary in 60 Seconds

  • Semantics: Record = immutable data carrier (“state is the header”); Class = encapsulated object (“state + behavior”).
  • Hierarchy: Records are implicitly final, always extend java.lang.Record, and cannot extend other classes. Interfaces are permitted.
  • Fields: Only private final components declared in the header. No extra instance fields.
  • Accessors: Named directly after components (user.email(), without get).
  • Constructors: Canonical, Compact (public User { ... } for validation), and secondary (must delegate to this(...)).
  • Security: Protected from reflection mutation and constructor-bypass deserialization attacks.
  • Java 21: Supports native Record Pattern deconstruction in instanceof and switch.

Common Interview Traps

  • Claiming Records are merely syntactic sugar for Lombok @Value (Records are a JVM platform feature with bytecode metadata and serialization guarantees).
  • Proposing Records as JPA @Entity classes (JPA requires no-arg constructors, non-final classes, and mutability).
  • Assuming Records provide deep immutability automatically without defensive copying.