Питання 2 · Розділ 20

У чому основні відмінності Record від звичайного класу

// java.lang.Record — спеціальний клас, який не можна використовувати напряму через extends.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Record — це спеціальний тип класу в Java, призначений для зберігання даних. Головна відмінність: Record створений бути простим контейнером даних, а звичайний клас — універсальним інструментом.

Основні відмінності:

| Record | Звичайний клас | | ———————————– | ——————— | | Всі поля private final | Поля будь-які | | Не можна змінити поля після створення | Можна змінювати поля | | Наслідується від java.lang.Record | Наслідується від Object | // java.lang.Record — спеціальний клас, який не можна використовувати напряму через extends. | implicit final — не можна розширювати | Можна наслідувати | | Автогенерація equals, hashCode, toString | Потрібно писати вручну або через IDE |

Приклад:

// Record — один рядок
public record Point(int x, int y) {}

// Звичайний клас — 30+ рядків
public class Point {
    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; }

    // ще equals, hashCode, toString...
}

🟡 Middle Level

Як це працює

Record — це обмежена форма класу, де компілятор бере на себе boilerplate:

1. Конструктор:

// Record — канонічний конструктор автогенерується
public record User(String name, int age) {}

// Можна додати свій конструктор (повинен викликати канонічний)
public record User(String name, int age) {
    public User(String name) {
        this(name, 0);  // делегування
    }
}

2. Геттери:

// Record — без префікса get!
User user = new User("John", 25);
user.name();  // ✅ НЕ user.getName()
user.age();   // ✅ НЕ user.getAge()

3. Наслідування:

// ❌ Record не можна розширити
public class Bad extends Point {}  // compilation error

// ❌ Record не може нічого розширювати (крім java.lang.Record)
public record Bad extends Object {}  // compilation error

// ✅ Record може імплементувати інтерфейси
public record User(String name) implements Serializable {}

Практичне застосування

Коли використовувати Record:

  • DTO для REST API
  • Ключі в HashMap/HashSet
  • Повернення кількох значень з методу
  • Value objects в DDD

Коли звичайний клас:

  • Потрібен mutable стан
  • Потрібні додаткові методи/поля
  • Потрібне наслідування
  • Потрібен builder pattern з mutable полями

Типові помилки

  1. Спроба зробити mutable поля: ```java public record BadRecord() { // ❌ Не static поле — помилка компіляції public int mutableField = 0; }

// ✅ Тільки static public record GoodRecord() { public static int counter = 0; }


2. **Очікування get/set методів:**
```java
public record User(String name) {}
User u = new User("John");

// ❌ u.getName() — такого немає!
// ✅ u.name() — правильний виклик

🔴 Senior Level

Internal Implementation

Class file structure:

// Record — це final class з ACC_RECORD прапорцем
ClassFile {
    access_flags: ACC_FINAL | ACC_RECORD
    super_class: java/lang/Record
    fields: всі private final
    methods: канонічний конструктор + аксесори + equals/hashCode/toString
}

Відмінності на рівні JVM:

  • Record має спеціальний атрибут RecordComponents в constant pool
  • JVM знає про канонічні компоненти (ім’я, тип, анотації)
  • Reflection API: Class.getRecordComponents() повертає RecordComponent[]

Архітектурні Trade-offs

Record:

  • ✅ Мінімум boilerplate
  • ✅ Гарантована іммутабельність
  • ✅ Оптимізація під value types (Project Valhalla)
  • ❌ Немає гнучкості
  • ❌ Не можна використовувати з JPA entities (потрібен no-arg конструктор)

Звичайний клас:

  • ✅ Повний контроль
  • ✅ JPA сумісність
  • ✅ Builder, mutable state
  • ❌ Більше коду
  • ❌ Ручна підтримка equals/hashCode

Edge Cases

1. Компактний конструктор для валідації:

public record Email(String value) {
    public Email {
        if (!value.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

2. Анотації на компонентах:

public record User(
    @JsonProperty("user_name") String name,
    @Min(18) int age
) {}
// Анотація застосовується до поля, параметру конструктора І аксесора

3. Серіалізація:

// Record використовує спеціальний механізм десеріалізації
// через канонічний конструктор (JEP 445, Java 21)
// замість readObject/writeObject

Продуктивність

Операція           | Record | Звичайний клас (final поля)
-------------------|--------|---------------------------
Створення об'єкта  | 8 ns   | 8 ns
equals()           | 15 ns  | 15 ns (ручна реалізація)
hashCode()         | 12 ns  | 12 ns

Різниця мінімальна — Record не поступається
// Приблизні значення. Залежать від JVM/hardware.

Production Experience

// DTO в Spring Boot 3.x — Records стали стандартом
public record CreateUserRequest(
    @NotBlank String name,
    @Email String email,
    @Min(18) int age
) {}

// JPA entities — поки не можна використовувати Records
// (потрібен mutable state, no-arg constructor, lazy loading proxies)
@Entity
public class User {  // звичайний клас
    @Id private Long id;
    private String name;
}

Best Practices

// ✅ Record для immutable DTO
public record OrderDto(String id, Instant createdAt, BigDecimal amount) {}

// ✅ Record для value objects
public record Money(BigDecimal amount, Currency currency) {
    public Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
    }
}

// ❌ Record для JPA entity
// ❌ Record для mutable колекцій без захисту
public record BadRecord(int[] values) {}  // масив mutable!

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Record — обмежений вид класу: тільки final поля, не можна наслідувати, не можна наслідувати самому
  • Record наслідується від java.lang.Record, звичайний клас — від Object
  • Автогенерація equals, hashCode, toString — Record vs ручний код у звичайному класі
  • Record immutable за дизайном; звичайний клас може бути mutable або immutable
  • Record не можна використовувати з JPA (потрібен no-arg конструктор, mutable state)
  • Record геттери без get: user.name() замість user.getName()

Часті уточнюючі запитання:

  • Коли обирати Record, а коли звичайний клас? — Record для immutable DTO/value objects, звичайний клас для JPA, mutable state, builder
  • Чи можна додати додаткові методи в Record? — Так, будь-які static та instance методи, але не instance поля
  • Record швидший за звичайний клас? — Ні, продуктивність ідентична, різниця < 5%
  • Чи можна перевизначити equals/hashCode в Record? — Так, але це рідко потрібно

Червоні прапорці (НЕ говорити):

  • ❌ “Record — це підклас звичайного класу” — Record наслідується від java.lang.Record, спеціального класу
  • ❌ “Record підтримує наслідування” — Record implicit final
  • ❌ “Record використовує get/set” — аксесори без get: name() не getName()
  • ❌ “Record підходить для JPA” — JPA вимагає no-arg конструктор і mutable поля

Пов’язані теми:

  • [[1. Що таке Record в Java і з якої версії вони доступні]]
  • [[3. Чи можна наслідуватися від Record або наслідувати Record від іншого класу]]
  • [[5. Які методи автоматично генеруються для Record]]
  • [[9. Чи є поля Record фінальними]]