Question 1 · Section 20

What is Record in Java and since which version are they available

Record allows you to declare a class in a single line, which automatically receives:

Language versions: English Russian Ukrainian

🟢 Junior Level

Record is a special type of class in Java designed for storing immutable data. It appeared as a preview feature in Java 14 (JEP 359) and became a full standard feature in Java 16 (JEP 395).

Record allows you to declare a class in a single line, which automatically receives:

  • private final fields
  • a public constructor
  • getters (without the get prefix)
  • equals(), hashCode() and toString()

Example:

// Instead of 50+ lines of code — one line
public record User(String name, int age, String email) {}

// Usage
User user = new User("John", 25, "john@example.com");
System.out.println(user.name());  // John
System.out.println(user.age());   // 25

When to use:

  • DTO (Data Transfer Objects)
  • Classes for storing query results from DB
  • Keys in collections (HashMap, HashSet)
  • Returning multiple values from a method
  • Any situation where you need a simple immutable class

🟡 Middle Level

How it works

Record is not just “syntactic sugar” — it’s a special kind of class with strict constraints:

// What you write:
public record Point(int x, int y) {}

// What the compiler generates:
public final class Point extends java.lang.Record {
    private final int x;
    private final int y;

    public Point(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) { /* auto-generated */ }

    @Override
    public int hashCode() { /* auto-generated */ }

    @Override
    public String toString() { /* auto-generated */ }
}

Practical Application

1. DTO in Spring Boot:

public record CreateUserRequest(
    @NotBlank String name,
    @Email String email,
    @Min(18) int age
) {}

@RestController
public class UserController {
    @PostMapping("/users")
    public ResponseEntity<UserResponse> createUser(
        @Valid @RequestBody CreateUserRequest request
    ) {
        // request.name(), request.email() — access fields
    }
}

2. Returning multiple values:

public record CalculationResult(int result, int operationTimeMs) {}

public CalculationResult calculate() {
    // ... complex logic
    return new CalculationResult(42, 150);
}

3. Pattern matching in switch (Java 21+):

public sealed interface Shape permits Circle, Rectangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}

public double getArea(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
    };
}

Common Mistakes

  1. Mistake: trying to make Record mutable
    // ❌ CANNOT — fields are always final
    public record BadRecord() {
        public int mutableField = 0;  // compilation error, unless static
    }
    
  2. Mistake: mutable components in Record
    // ⚠️ Caution — array is mutable!
    public record DataWithArray(int[] values) {}
    
    DataWithArray d1 = new DataWithArray(new int[]{1, 2, 3});
    d1.values()[0] = 99;  // can be changed!
    
    // ✅ Better — use List.of() or make a copy
    public record SafeData(List<Integer> values) {}
    
  3. Mistake: null in Record
    // Record allows null by default
    public record User(String name) {}
    User user = new User(null);  // OK, but may lead to NPE
    
    // ✅ Solution — null check in constructor (Java 16+)
    public record User(String name) {
        public User {
            Objects.requireNonNull(name, "Name cannot be null");
        }
    }
    

Comparison with Alternatives

Approach Pros Cons
Record Minimal code, immutable, auto-generation Cannot inherit, only final fields
Lombok @Data More flexibility, mutable/immutable Lombok dependency, IDE issues
Manual class Full control Lots of boilerplate, errors in equals/hashCode
Class with @Value Immutable, clear More code than Record

🔴 Senior Level

Internal Implementation

Record is not just syntactic sugar — it’s a new data model in the JVM.

Key JEPs:

  • JEP 359: Records (Preview, Java 14)
  • JEP 384: Records (Second Preview, Java 15)
  • JEP 395: Records (Final, Java 16)

Internal structure:

// Compiler adds hidden attributes:
// - ACC_RECORD flag in class file
// - RecordComponents attribute in constant pool

// At runtime you can get components via reflection:
RecordComponent[] components = Point.class.getRecordComponents();
for (RecordComponent rc : components) {
    System.out.println(rc.getName());     // "x", "y"
    System.out.println(rc.getType());     // int, int
    System.out.println(rc.getAccessor()); // Method reference
}

Class file structure:

ClassFile {
    u2 access_flags;        // ACC_RECORD | ACC_FINAL
    u2 this_class;
    u2 super_class;         // java/lang/Record
    ...
    Record_attribute {
        u2 attribute_name_index;
        u4 attribute_length;
        u2 components_count;
        record_component_info components[];
    }
}

Architectural Trade-offs

Record vs regular class:

Aspect Record Regular class
Inheritance Not allowed (implicit final) Allowed
Fields Only final Any
Interfaces Can implement Can
sealed Can Can
Annotations On fields, constructor On anything
Serialization Own logic (JEP 445) Standard

Why Record cannot be extended:

java.lang.Record — special superclass not available for direct extends.
This is done for:
1. Immutability guarantee
2. Predictable equals/hashCode
3. JVM optimization (value types compatibility)
4. Pattern matching support

Edge Cases

1. Generic Records:

public record ApiResponse<T>(int status, T data, String message) {}

// Usage
ApiResponse<List<User>> response = new ApiResponse<>(200, users, "OK");
ApiResponse<Optional<User>> maybeUser = new ApiResponse<>(404, Optional.empty(), "Not found");

2. Recursive Records (for linked structures):

public record ListNode<T>(T value, ListNode<T> next) {}

ListNode<String> list = new ListNode<>("A",
    new ListNode<>("B",
        new ListNode<>("C", null)));

3. Records with annotations:

public record User(
    @JsonProperty("user_name") @NonNull String name,
    @Min(0) @Max(150) int age
) {
    // Annotation on component applies to:
    // - field
    // - constructor parameter
    // - accessor method
}

// For precise control — annotations on target elements:
public record Config(
    String key
) {}
// In Java, annotations on Record components apply to field, parameter, and accessor.
// For targeted application, use meta-annotations @Target({FIELD, METHOD, PARAMETER}).

4. Records and Serialization (Java 21+):

// Record serialization: during deserialization the canonical constructor is called,
// not readObject. This is standard Java serialization (not JEP 445).

record SerializableData(String name, int value) implements Serializable {}

// JVM automatically validates:
// 1. All final fields are initialized
// 2. Invariants are preserved
// 3. Compact constructor can be used for validation

Performance

Memory:

Record vs Class with same functionality:
- Object header: 12-16 bytes (depends on JVM)
- Fields: as usual (int = 4, long = 8, reference = 4-8)
- Lombok generates code at compile time — no runtime overhead in either case.
- No extra fields for equals/hashCode cache

Execution time:

equals() / hashCode() auto-generated:
- Compiler creates optimal implementation
- Uses Objects.hash() or Arrays.hashCode()
- Performance comparable to manual implementation
- JIT can inline better (final fields)

Benchmark (JMH, Java 21):

// Approximate values (JMH, Java 21). Depend on JVM and hardware.
Operation          | Record | Lombok @Value | Manual class
-------------------|--------|---------------|--------------
equals()           | 15 ns  | 15 ns         | 14 ns
hashCode()         | 12 ns  | 13 ns         | 12 ns
toString()         | 45 ns  | 48 ns         | 46 ns
Object creation    | 8 ns   | 8 ns          | 8 ns

Difference < 5% — Record is not inferior in performance

Production Experience

Real case — migrating DTOs to Records:

// Before (Spring Boot 2.x, Lombok):
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    private String name;
    private String email;
    private int age;
}

// After (Spring Boot 3.x, Java 17+):
public record UserDto(String name, String email, int age) {}

// Result:
// - Code reduced from ~15 lines to 1
// - Removed Lombok dependency for these classes
// - DTOs became immutable (protection from accidental changes)
// - Jackson serializes automatically (via constructor)

Issue with Jackson and Records:

// Jackson 2.12+ supports Records natively
// But there may be nuances:

public record Order(@JsonProperty("order_id") String id) {}

// ✅ Works out of the box in Spring Boot 2.4+
// ⚠️ In older versions you need:
// 1. Update Jackson to 2.12+
// 2. Or add @JsonCreator on canonical constructor

Monitoring

Debugging Records:

# Decompile .class file
javap -p UserRecord.class

# Output will show:
# final class UserRecord extends java.lang.Record
# private final java.lang.String name
# private final int age
# public UserRecord(java.lang.String, int)
# public java.lang.String name()
# public int age()

Reflection for inspection:

public static void inspectRecord(Object record) {
    Class<?> clazz = record.getClass();
    if (!clazz.isRecord()) {
        throw new IllegalArgumentException("Not a record");
    }

    RecordComponent[] components = clazz.getRecordComponents();
    System.out.println("Record: " + clazz.getSimpleName());
    for (RecordComponent rc : components) {
        Method accessor = rc.getAccessor();
        Object value = accessor.invoke(record);
        System.out.println("  " + rc.getName() + " = " + value);
    }
}

Java 21+:

// Record Patterns (JEP 440) — Pattern matching for Record
Point p = new Point(42, 0);
if (p instanceof Point(int x, int y)) {
    System.out.println(x + y);  // 42
}

// Switch with Record Patterns
double calcArea(Shape s) {
    return switch (s) {
        case Circle(double r) -> Math.PI * r * r;
        case Rectangle(double w, double h) -> w * h;
        case Triangle(double b, double h) -> 0.5 * b * h;
    };
}

// Sealed + Records + Pattern Matching = powerful combo
public sealed interface Expr
    permits Num, Add, Mul {}

public record Num(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}

int eval(Expr e) {
    return switch (e) {
        case Num(int n) -> n;
        case Add(Expr l, Expr r) -> eval(l) + eval(r);
        case Mul(Expr l, Expr r) -> eval(l) * eval(r);
    };
}

Best Practices for Highload

// 1. Use Records for immutable DTO
public record OrderDto(String id, Instant createdAt, BigDecimal amount) {}

// 2. Validation in compact constructor
public record UserId(String value) {
    public UserId {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("Invalid ID");
        }
        if (value.length() > 36) {
            throw new IllegalArgumentException("ID too long");
        }
    }
}

// 3. Records as cache keys — ideal (immutable + hashCode)
public record CacheKey(String tenantId, String entityType, String entityId) {}

Map<CacheKey, Object> cache = new ConcurrentHashMap<>();

// 4. Use for event sourcing
public record DomainEvent(
    UUID eventId,
    String aggregateId,
    String eventType,
    Instant occurredAt,
    Map<String, Object> payload
) {}

🎯 Interview Cheat Sheet

Must know:

  • Records appeared as preview in Java 14 (JEP 359), became stable in Java 16 (JEP 395)
  • Record is a special type of class that inherits java.lang.Record
  • Auto-generates: canonical constructor, accessors, equals(), hashCode(), toString()
  • Accessors are named after the field: user.name(), NOT user.getName()
  • Record is ideal for DTO, HashMap keys, value objects
  • Record does not support inheritance — implicit final
  • Starting from Java 21, pattern matching for Records is available (JEP 440)

Common follow-up questions:

  • Since which version are Records stable? — Java 16 (JEP 395), preview — Java 14
  • Can you make a Record mutable? — No, all fields are automatically final
  • How does Record work with Jackson? — Jackson 2.12+ supports natively via canonical constructor
  • How does Record differ from Lombok @Value? — Record is a language standard, less code, no Lombok dependency

Red flags (DO NOT say):

  • ❌ “Record is just syntactic sugar for Lombok” — Record is a full data model in the JVM
  • ❌ “You can inherit Record” — Record is implicit final, inheritance is forbidden
  • ❌ “Record has get/set methods” — accessors without get prefix: name() instead of getName()
  • ❌ “Record is slower than a regular class” — performance is identical (difference < 5%)

Related topics:

  • [[2. What are the main differences between Record and regular class]]
  • [[5. What methods are automatically generated for a Record]]
  • [[9. Are Record fields final]]
  • [[10. Can you use Record as a key in HashMap]]