Вопрос 1 · Раздел 20

Что такое Record в Java и с какой версии они доступны

Record позволяет в одну строку объявить класс, который автоматически получает:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Record — это специальный тип класса в Java, предназначенный для хранения неизменяемых данных. Он появился как превью-фича в Java 14 (JEP 359) и стал полноценной стандартной фичей в Java 16 (JEP 395).

Record позволяет в одну строку объявить класс, который автоматически получает:

  • приватные final поля
  • публичный конструктор
  • геттеры (без приставки get)
  • equals(), hashCode() и toString()

Пример:

// Вместо 50+ строк кода — одна строка
public record User(String name, int age, String email) {}

// Использование
User user = new User("John", 25, "john@example.com");
System.out.println(user.name());  // John
System.out.println(user.age());   // 25

Когда использовать:

  • DTO (Data Transfer Objects)
  • Классы для хранения результатов запросов из БД
  • Ключи в коллекциях (HashMap, HashSet)
  • Возврат нескольких значений из метода
  • Любые ситуации, когда нужен простой immutable-класс

🟡 Middle Level

Как это работает

Record — это не просто “сахар”, это особый вид класса с жёсткими ограничениями:

// То, что вы пишете:
public record Point(int x, int y) {}

// То, что генерирует компилятор:
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) { /* автогенерированный */ }
    
    @Override
    public int hashCode() { /* автогенерированный */ }
    
    @Override
    public String toString() { /* автогенерированный */ }
}

Практическое применение

1. DTO в 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() — доступ к полям
    }
}

2. Возврат нескольких значений:

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

public CalculationResult calculate() {
    // ... сложная логика
    return new CalculationResult(42, 150);
}

3. Pattern matching в 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();
    };
}

Типичные ошибки

  1. Ошибка: попытка сделать Record mutable
    // ❌ НЕЛЬЗЯ — поля всегда final
    public record BadRecord() {
        public int mutableField = 0;  // compilation error, если не static
    }
    
  2. Ошибка: мутабельные компоненты в Record
    // ⚠️ Осторожно — массив mutable!
    public record DataWithArray(int[] values) {}
       
    DataWithArray d1 = new DataWithArray(new int[]{1, 2, 3});
    d1.values()[0] = 99;  // можно изменить!
       
    // ✅ Лучше — использовать List.of() или делать копию
    public record SafeData(List<Integer> values) {}
    
  3. Ошибка: null в Record
    // Record допускает null по умолчанию
    public record User(String name) {}
    User user = new User(null);  // OK, но может привести к NPE
       
    // ✅ Решение — null check в конструкторе (Java 16+)
    public record User(String name) {
        public User {
            Objects.requireNonNull(name, "Name cannot be null");
        }
    }
    

Сравнение с альтернативами

Подход Плюсы Минусы
Record Минимум кода, immutable, автогенерация Нельзя наследовать, только final поля
Lombok @Data Больше гибкости, mutable/immutable Зависимость от Lombok, проблемы с IDE
Ручной класс Полный контроль Много boilerplate, ошибки в equals/hashCode
Класс с @Value Immutable, понятный Больше кода, чем Record

🔴 Senior Level

Internal Implementation

Record — это не просто syntactic sugar, это новая модель данных в JVM.

Ключевые JEP:

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

Внутреннее устройство:

// Компилятор добавляет скрытые атрибуты:
// - ACC_RECORD flag в class file
// - RecordComponents attribute в constant pool

// Runtime можно получить компоненты через 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[];
    }
}

Архитектурные Trade-offs

Record vs обычный класс:

Аспект Record Обычный класс
Наследование Нельзя (implicit final) Можно
Поля Только final Любые
Интерфейсы Можно имплементировать Можно
sealed Можно Можно
Аннотации На поля, конструктор На что угодно
Serialization Своя логика (JEP 445) Стандартная

Почему нельзя расширять Record:

java.lang.Record — специальный суперкласс, недоступный для прямого extends.
Это сделано для:
1. Гарантии иммутабельности
2. Предсказуемого equals/hashCode
3. Оптимизации JVM (value types совместимость)
4. Pattern matching работы

Edge Cases

1. Generic Records:

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

// Использование
ApiResponse<List<User>> response = new ApiResponse<>(200, users, "OK");
ApiResponse<Optional<User>> maybeUser = new ApiResponse<>(404, Optional.empty(), "Not found");

2. Recursive Records (для linked структур):

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

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

3. Records с аннотациями:

public record User(
    @JsonProperty("user_name") @NonNull String name,
    @Min(0) @Max(150) int age
) {
    // Аннотация на уровне компонента применяется к:
    // - полю
    // - параметру конструктора
    // - методу-аксессору
}

// Для точного контроля — аннотации на целевые элементы:
public record Config(
    String key
) {}
// В Java аннотации на компонентах Record применяются к полю, параметру и аксессору.
// Для целевого применения используйте meta-аннотации @Target({FIELD, METHOD, PARAMETER}).

4. Records и Serialization (Java 21+):

// Record serialization: при десериализации вызывается канонический конструктор,
// а не readObject. Это стандартная Java serialization (не JEP 445).

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

// JVM автоматически валидирует:
// 1. Все финальные поля инициализированы
// 2. Инварианты сохранены
// 3. Можно использовать компактный конструктор для валидации

Производительность

Память:

Record vs Class с тем же функционалом:
- Заголовок объекта: 12-16 байт (зависит от JVM)
- Поля: как обычно (int = 4, long = 8, reference = 4-8)
- Lombok генерирует код на этапе компиляции — runtime overhead отсутствует в обоих случаях.
- Нет дополнительных полей для equals/hashCode кеша

Время выполнения:

equals() / hashCode() автогенерированные:
- Компилятор создаёт оптимальную реализацию
- Использует Objects.hash() или Arrays.hashCode()
- Performance сравним с ручной реализацией
- JIT может инлайнить лучше (final поля)

Бенчмарк (JMH, Java 21):

// Примерные значения (JMH, Java 21). Зависят от JVM и hardware.
Операция           | Record | Lombok @Value | Ручной класс
-------------------|--------|---------------|--------------
equals()           | 15 ns  | 15 ns         | 14 ns
hashCode()         | 12 ns  | 13 ns         | 12 ns
toString()         | 45 ns  | 48 ns         | 46 ns
Создание объекта    | 8 ns   | 8 ns          | 8 ns

Разница < 5% — Record не уступает по производительности

Production Experience

Реальный кейс — миграция DTO на Records:

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

// После (Spring Boot 3.x, Java 17+):
public record UserDto(String name, String email, int age) {}

// Результат:
// - Код сократился с ~15 строк до 1
// - Убрали зависимость Lombok для этих классов
// - DTO стали immutable (защита от случайных изменений)
// - Jackson сериализует автоматически (через конструктор)

Проблема с Jackson и Records:

// Jackson 2.12+ поддерживает Records нативно
// Но могут быть нюансы:

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

// ✅ Работает из коробки в Spring Boot 2.4+
// ⚠️ В старых версиях нужно:
// 1. Обновить Jackson до 2.12+
// 2. Или добавить @JsonCreator на канонический конструктор

Monitoring

Отладка Records:

# Декомпиляция .class файла
javap -p UserRecord.class

# Вывод покажет:
# 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()

Рефлексия для инспекции:

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 для Record
Point p = new Point(42, 0);
if (p instanceof Point(int x, int y)) {
    System.out.println(x + y);  // 42
}

// Switch с 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 = мощный набор
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 для Highload

// 1. Используйте Records для immutable DTO
public record OrderDto(String id, Instant createdAt, BigDecimal amount) {}

// 2. Валидация в компактном конструкторе
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 как ключи в кеше — идеально (immutable + hashCode)
public record CacheKey(String tenantId, String entityType, String entityId) {}

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

// 4. Используйте для event sourcing
public record DomainEvent(
    UUID eventId,
    String aggregateId,
    String eventType,
    Instant occurredAt,
    Map<String, Object> payload
) {}

🎯 Шпаргалка для интервью

Обязательно знать:

  • Records появились как preview в Java 14 (JEP 359), стали стабильными в Java 16 (JEP 395)
  • Record — это специальный тип класса, наследующий java.lang.Record
  • Автоматически генерирует: канонический конструктор, аксессоры, equals(), hashCode(), toString()
  • Аксессоры называются по имени поля: user.name(), а НЕ user.getName()
  • Record идеально подходит для DTO, ключей в HashMap, value objects
  • Record не поддерживает наследование — implicit final
  • Начиная с Java 21 доступен pattern matching для Records (JEP 440)

Частые уточняющие вопросы:

  • С какой версии Records стали стабильными? — Java 16 (JEP 395), preview — Java 14
  • Можно ли сделать Record mutable? — Нет, все поля автоматически final
  • Как Record работает с Jackson? — Jackson 2.12+ поддерживает нативно через канонический конструктор
  • Чем Record отличается от Lombok @Value? — Record — стандарт языка, меньше кода, нет зависимости от Lombok

Красные флаги (НЕ говорить):

  • ❌ “Record — это просто syntactic sugar для Lombok” — Record — полноценная модель данных в JVM
  • ❌ “Можно наследовать Record” — Record implicit final, наследование запрещено
  • ❌ “Record имеет get/set методы” — аксессоры без приставки get: name() вместо getName()
  • ❌ “Record медленнее обычного класса” — производительность идентична (разница < 5%)

Связанные темы:

  • [[2. В чём основные отличия Record от обычного класса]]
  • [[5. Какие методы автоматически генерируются для Record]]
  • [[9. Являются ли поля Record финальными]]
  • [[10. Можно ли использовать Record как ключ в HashMap]]