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

Чи є поля Record фінальними

Це робить Record immutable (незмінюваним) за дизайном.

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

🟢 Junior Level

Так, всі поля в Record автоматично є final. Це означає, що їх не можна змінити після створення об’єкта.

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

User user = new User("John", 25);

// ❌ Не можна змінити поля
user.name = "Jane";  // compilation error — поле final

// ✅ Можна створити новий Record
user = new User("Jane", 25);  // OK

Це робить Record immutable (незмінюваним) за дизайном.


🟡 Middle Level

Як це працює

Компілятор автоматично робить всі поля private final:

public record Point(int x, int y) {}

// Компілятор генерує:
public final class Point extends java.lang.Record {
    private final int x;  // implicit final
    private final int y;  // implicit final

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }
}

Навіть якщо ви не написали final:

// Обидва варіанти однакові
public record Point(int x, int y) {}
public record Point(final int x, final int y) {}  // final надмірний

Важливий нюанс: mutable компоненти

Поля final, але якщо вони посилаються на mutable об’єкти, то вміст можна змінити:

public record User(String name, List<String> tags) {}

User user = new User("John", new ArrayList<>(List.of("admin", "user")));

// ❌ Не можна замінити список
// user.tags = new ArrayList<>();  // compilation error

// ✅ Але можна змінити вміст списку
user.tags().add("moderator");  // OK! Список mutable
user.tags().clear();           // OK!

Рішення — defensive copy:

public record User(String name, List<String> tags) {
    public User {
        tags = List.copyOf(tags);  // immutable список
    }
}

User user = new User("John", List.of("admin"));
// user.tags().add("moderator");  // UnsupportedOperationException!

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

  1. Очікування повної іммутабельності: ```java public record Data(int[] values) {}

Data d = new Data(new int[]{1, 2, 3}); d.values()[0] = 99; // ✅ Можна — масив mutable!

// ✅ Рішення public record SafeData(List values) { public SafeData { values = List.copyOf(values); } }


2. **Спроба змінити поле:**
```java
public record Point(int x, int y) {
    public void setX(int x) {
        // this.x = x;  // compilation error — final поле
    }
}

🔴 Senior Level

Internal Implementation

JVM level:

// Всі компоненти Record мають ACC_FINAL прапорець
Field {
    access_flags: ACC_PRIVATE | ACC_FINAL
    name: "x"
    descriptor: "I"
}

Reflection підтверджує:

Field[] fields = Point.class.getDeclaredFields();
for (Field f : fields) {
    System.out.println(f.getName() + " final=" + Modifier.isFinal(f.getModifiers()));
}
// Вивід: x final=true, y final=true

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

Final поля:

Плюси Мінуси
Thread-safe (safe publication) Не можна змінити стан
JIT оптимізації Потрібно створювати нові об’єкти
Передбачуваність Mutable компоненти вимагають захисту
HashMap ключі Складше працювати з графами

Edge Cases

1. Mutable компоненти:

public record MutableRecord(Date createdAt) {}

MutableRecord r = new MutableRecord(new Date());
r.createdAt().setTime(0);  // можна змінити Date!

// ✅ Рішення
public record ImmutableRecord(Date createdAt) {
    public ImmutableRecord {
        createdAt = new Date(createdAt.getTime());  // defensive copy
    }
}

2. Final ≠ immutable:

public record Config(Map<String, String> props) {}

// final поле, але Map можна змінити
Config c = new Config(new HashMap<>());
c.props().put("key", "value");  // OK

3. Memory visibility:

// final поля гарантують safe publication
// після завершення конструктора всі потоки бачать коректні значення
public record SharedConfig(String url, int timeout) {}

// Publication safe
SharedConfig config = new SharedConfig("http://api.com", 5000);
// Інший потік побачить коректні значення

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

Final поля:
- JIT може інлайнити значення
- Немає необхідності в volatile
- Safe publication без синхронізації
- Memory barrier при конструюванні

Бенчмарк:

// Приблизні значення. Залежать від JVM/hardware.
Операція           | Final поля | Mutable поля
-------------------|------------|-------------
Read                | 1 ns       | 1 ns
Publish (thread)    | 0 ns       | sync needed
JIT optimization    | Максимальна | Обмежена

Production Experience

Immutable DTO:

public record OrderDto(
    String id,
    Instant createdAt,
    BigDecimal amount,
    List<OrderItem> items
) {
    public OrderDto {
        items = List.copyOf(items);  // захист mutable колекції
    }
}

Value objects:

public record Money(BigDecimal amount, Currency currency) {
    // Обидва поля final — повністю immutable
    public Money add(Money other) {
        // Повертаємо новий об'єкт, не змінюємо поточний
        return new Money(this.amount.add(other.amount), this.currency);
    }
}

Best Practices

// ✅ Record для immutable даних
public record User(String name, Email email) {}

// ✅ Defensive copy для mutable компонентів
public record Tags(List<String> values) {
    public Tags { values = List.copyOf(values); }
}

// ✅ Immutable колекції
public record Config(Map<String, String> props) {
    public Config { props = Map.copyOf(props); }
}

// ❌ Mutable компоненти без захисту
public record BadRecord(Date date, int[] array) {}
// ❌ Очікування що final = повністю immutable

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

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

  • Всі поля Record автоматично private final — компілятор додає final
  • Final означає не можна замінити посилання, але mutable об’єкти всередині можна змінити
  • Масиви, Date, колекції — mutable компоненти, вимагають defensive copy
  • List.copyOf(), Set.copyOf(), Map.copyOf() для захисту mutable колекцій
  • Final поля гарантують safe publication в багатопоточному середовищі (без volatile)
  • JIT може краще оптимізувати final поля (inlining)

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

  • Чи можна змінити масив всередині Record? — Так, масив mutable: record.values()[0] = 99
  • Як захистити mutable компонент? — Defensive copy в компактному конструкторі: tags = List.copyOf(tags)
  • Final = повністю immutable? — Ні, final захищає тільки посилання, не вміст об’єкта
  • Чому final важливий для HashMap ключів? — Змінений ключ = зламаний hashCode, дані втрачені

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

  • ❌ “Final означає повну іммутабельність” — Mutable компоненти можна змінити
  • ❌ “Масив в Record безпечний” — Масиви завжди mutable, потрібен defensive copy
  • ❌ “Можна змінити поле через setter” — Поля final, setter неможливий
  • ❌ “Final не дає переваг” — Final дає safe publication та JIT оптимізації

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

  • [[1. Що таке Record в Java і з якої версії вони доступні]]
  • [[5. Які методи автоматично генеруються для Record]]
  • [[7. Що таке компактний конструктор (compact constructor) в Record]]
  • [[10. Чи можна використовувати Record як ключ в HashMap]]