Питання 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]]