📋 Section 20 · Question #1

What is Record in Java and Since Which Version are They Available

A record in Java is a specialized class type designed as a transparent, immutable data carrier.


🟢 Junior Level

A record in Java is a specialized class type designed as a transparent, immutable data carrier.

Java Version Timeline

  • Java 14 (March 2020): First preview (JEP 359).
  • Java 15 (September 2020): Second preview (JEP 384).
  • Java 16 (March 2021): General Availability (GA) (JEP 395). Starting in Java 16, Records are a permanent standard feature available without --enable-preview flags.

The Problem Records Solve

Historically, creating a simple immutable Data Transfer Object (DTO) or Value Object in Java required 40–60 lines of repetitive boilerplate code:

  • Private final fields
  • All-arguments constructor
  • Public accessor methods (getters)
  • Value-based equals() and hashCode()
  • Informative toString()

In modern Java, this entire contract is declared in a single line:

public record User(String name, int age, String email) {}

The compiler automatically synthesizes:

  1. private final fields for each component (name, age, email).
  2. A public canonical constructor accepting all components as arguments.
  3. Public accessor methods matching component names: user.name(), user.age(), user.email() (without the legacy get prefix).
  4. Value-based equals() and hashCode() comparing all declared components.
  5. A readable toString() method, e.g., User[name=Alice, age=30, email=alice@example.com].
User user = new User("Alice", 30, "alice@example.com");
System.out.println(user.name()); // "Alice"
System.out.println(user);        // User[name=Alice, age=30, email=alice@example.com]

🟡 Middle Level

1. Architectural Invariants of Records

Records are not merely syntactic sugar; they are a distinct language construct governed by strict architectural rules:

  1. Implicitly final Class: A record cannot be extended by another class, nor can it declare abstract.
  2. Fixed Superclass: All records implicitly extend java.lang.Record. Explicit class inheritance (record Point(...) extends AnyClass) is prohibited by the compiler.
  3. Interfaces are Allowed: A record can implement any number of interfaces:
    public record Point(int x, int y) implements Serializable, Comparable<Point> {
        @Override
        public int compareTo(Point o) {
            return Integer.compare(this.x, o.x);
        }
    }
    
  4. No Additional Instance Fields: A record body cannot declare additional non-static instance fields:
    public record Point(int x, int y) {
        // private int z; // ❌ Compilation error: non-static field not allowed in record
        public static final Point ORIGIN = new Point(0, 0); // ✅ Static fields are permitted
    }
    

2. Compact Constructor

A key feature of Records is the compact constructor, which omits the parameter list and is used for input validation and normalization:

public record Money(BigDecimal amount, Currency currency) {
    // Compact constructor: amount and currency parameters are passed implicitly
    public Money {
        Objects.requireNonNull(amount, "Amount must not be null");
        Objects.requireNonNull(currency, "Currency must not be null");
        if (amount.scale() > 2) {
            // Value normalization: reassigning updates the implicit local constructor parameter
            amount = amount.setScale(2, RoundingMode.HALF_UP);
        }
        // The compiler automatically performs: this.amount = amount; this.currency = currency;
    }
}

3. Shallow Immutability Warning

[!WARNING] A record guarantees immutability of its component references, but not the internal state of the referenced objects! If a record holds a mutable object (e.g., ArrayList, Date, or an array), its contents can be mutated externally:

public record BadRecord(List<String> items) {}

List<String> mutableList = new ArrayList<>(List.of("A", "B"));
BadRecord record = new BadRecord(mutableList);
mutableList.add("C"); // Mutates internal state of record!
record.items().clear(); // Also mutates!

// ✅ Solution: Defensive Copying in the compact constructor:
public record SafeRecord(List<String> items) {
    public SafeRecord {
        items = List.copyOf(items); // Creates deeply unmodifiable list
    }
}

🔴 Senior Level

Bytecode Representation and JVM Reflection

The compiler marks a record class in the .class file with specialized metadata:

  • Access flag: ACC_RECORD (0x10000).
  • Superclass in the constant pool: java/lang/Record.
  • A dedicated class-level bytecode attribute: Record:
    Record_attribute {
        u2 attribute_name_index;
        u4 attribute_length;
        u2 components_count;
        record_component_info components[components_count];
    }
    

This native structural attribute allows the Java Reflection API to inspect components directly:

RecordComponent[] components = User.class.getRecordComponents();
for (RecordComponent rc : components) {
    String name = rc.getName();          // "name", "age"
    Class<?> type = rc.getType();        // String.class, int.class
    Method accessor = rc.getAccessor();  // public String User.name()
}

Modern serialization frameworks (Jackson 2.12+) and object mappers leverage getRecordComponents() for fast, deterministic reflection without heuristic getter guessing.

The Serialization Revolution

Standard Java Serialization (implements Serializable) was historically the primary vector for JVM vulnerabilities. For regular classes, deserialization bypasses constructors entirely, instantiating instances via native reflection mechanisms (ReflectionFactory.newConstructorForSerialization) and writing fields directly into memory via Unsafe.putObject. This allowed attackers to inject corrupted states violating class invariants.

For Records, this vulnerability is closed at the JVM specification level:

  1. During deserialization, ObjectInputStream reads component values from the stream and mandatorily passes them into the record’s canonical constructor!
  2. All defensive checks and validation routines inside the compact or canonical constructor execute deterministically, even if the byte stream was manipulated.
  3. Special customization methods like readObject, writeObject, and readObjectNoData are explicitly ignored and forbidden for records.

Reflection Immutability (JEP 395 / JEP 396)

In regular classes, private final fields can be mutated via reflection:

Field field = OldClass.class.getDeclaredField("value");
field.setAccessible(true);
field.set(instance, newValue); // Allowed in older Java versions

For Record components, the JVM enforces hard immutability: calling field.set(...) unconditionally throws IllegalAccessException: Can not set final field of a record class, even after field.setAccessible(true).

Project Valhalla Alignment

Records are designed to transition naturally into Value Objects (Identity-free classes) under Project Valhalla. Because records already enforce component transparency and immutability, the JVM will eventually be able to flatten them directly onto the thread stack or pack them into contiguous arrays without object headers or pointer indirection.


4 Tricky Questions

1. How does deserialization of Records differ from regular Java classes, and why does this eliminate major security exploits?

Answer: Standard deserialization of ordinary classes bypasses all constructors using native Unsafe allocation, directly writing serialized bytes into memory fields without validation. This historically allowed malicious payloads to construct invalid objects (e.g., negative balances or bypassed security tokens). For Record classes, the JVM specification mandates that ObjectInputStream reconstructs instances exclusively by invoking the canonical constructor, passing serialized component values as constructor arguments. Consequently, all validation rules declared in compact or canonical constructors execute unconditionally during deserialization, preventing invalid or malicious states from being instantiated.

2. Can a private final field of a Record be modified at runtime using Reflection (field.setAccessible(true); field.set(...))?

Answer: No, it is strictly impossible. Starting with Java 16 (JEP 395) and strong JVM encapsulation, the implementation of Field.set() explicitly inspects whether the declaring class is a record (clazz.isRecord()). If so, it unconditionally throws IllegalAccessException: Can not set final field of a record class. Component fields of records are immutable at the JVM runtime level.

3. Why did the Java designers omit the traditional get prefix from Record accessors (e.g. user.name() instead of user.getName())?

Answer: The creators intentionally departed from the 1996 JavaBeans naming convention because a Record is not a mutable JavaBean; it is a mathematical tuple and transparent data carrier. The component accessor represents the mathematical projection of the state header rather than a state-encapsulated getter. Modern frameworks (Jackson, Spring, Hibernate 6, MapStruct) natively detect and bind record accessors.

4. Can you declare a Record locally inside a method, and can it capture local variables from the enclosing method scope?

Answer: Yes, Local Records can be declared inside method bodies (standardized in Java 16). They are widely used for intermediate tuple calculations in Stream API pipelines. However, a local record is implicitly static. Unlike local inner classes or anonymous classes, a local record cannot capture non-static local variables or access instance fields of the enclosing class. It can operate solely on data explicitly passed to its components or static constants.


🎯 Interview Cheat Sheet

Summary in 60 Seconds

  • Versions: Preview in Java 14/15; General Availability in Java 16 (JEP 395).
  • Core Concept: Transparent, immutable data carrier subclassing java.lang.Record.
  • Auto-Generated: private final fields, canonical constructor, accessors (name(), no get), equals(), hashCode(), and toString().
  • Constructors: Canonical, Compact (validation/normalization), and custom overloaded (must delegate via this(...)).
  • Restrictions: Cannot extend classes, cannot be extended (final), cannot declare additional instance fields.
  • Serialization: Secure by design; always instantiates via canonical constructor.
  • Reflection: Immune to reflection modification via Field.set().

Common Interview Traps

  • Assuming records provide deep immutability automatically (mutable objects like ArrayList require defensive copies via List.copyOf).
  • Attempting to use a Record as a JPA @Entity (JPA entities require no-arg constructors and mutable proxying; use Records for @Query DTO projections instead).
  • Expecting JavaBean-style getters like getName() instead of name().