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

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

Компілятор Java неявно додає модифікатор final до кожного поля екземпляра, оголошеного в заголовку Record. Змінити посилання на поле після конструювання об'єкта неможливо.


🟢 Junior Level

Так, абсолютно всі поля в Record автоматично є private final.

Компілятор Java неявно додає модифікатор final до кожного поля екземпляра, оголошеного в заголовку Record. Змінити посилання на поле після конструювання об’єкта неможливо.

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

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

// ❌ Помилка компіляції: Cannot assign a value to final variable 'name'
user.name = "Bob"; 

// ❌ Спроба зімітувати сетер не скомпільується:
// public void setAge(int age) { this.age = age; } // Cannot assign a value to final variable

// ✅ Щоб змінити дані, створюється новий екземпляр (незмінне оновлення):
User updatedUser = new User(user.name(), 26);

Явне зазначення модифікатора final у заголовку запису синтаксично допустиме, але є надлишковим:

public record Point(final int x, final int y) {} // final тут надлишковий

🟡 Middle Level

1. Shallow Immutability vs Deep Immutability (Поверхнева vs Глибока незмінність)

[!CAUTION] Модифікатор final на полі гарантує незмінність лише самого посилання, але не вмісту об’єкта, на який воно вказує!

Якщо компонент Record посилається на мутабельний об’єкт (наприклад, ArrayList, Date або масив примітивів), об’єкт можна мутувати ззовні:

public record ShoppingCart(String user, List<String> items) {}

List<String> mutableList = new ArrayList<>();
mutableList.add("Apple");

ShoppingCart cart = new ShoppingCart("Alice", mutableList);

// Мутація через зовнішнє посилання:
mutableList.add("Banana"); 
System.out.println(cart.items()); // ["Apple", "Banana"] — стан кошика змінився!

// Мутація через виклик гетера:
cart.items().clear();
System.out.println(cart.items()); // [] — кошик спустошено!

2. Як досягти глибокої незмінності (Deep Immutability)

Для гарантії 100% незмінності використовуйте двостороннє захисне копіювання (Defensive Copying):

  1. Для колекцій: використовуйте List.copyOf(), Set.copyOf(), Map.copyOf() у компактному конструкторі:
    public record ShoppingCart(String user, List<String> items) {
        public ShoppingCart {
            // List.copyOf повертає суворо незмінний список і захищає від зовнішніх мутацій:
            items = List.copyOf(items);
        }
    }
    
  2. Для масивів: потрібне захисне копіювання і в конструкторі, і в аксесорі:
    public record Buffer(byte[] data) {
        public Buffer {
            data = data.clone(); // Захист при створенні
        }
    
        @Override
        public byte[] data() {
            return data.clone(); // Захист при читанні
        }
    }
    

🔴 Senior Level

Семантика Моделі пам’яті Java (JMM) та Безпечна публікація (Safe Publication)

Завдяки тому, що всі поля запису є final, на них повною мірою поширюється семантика final-полів специфікації Java Memory Model (JLS §17.5).

Як це працює на рівні апаратного забезпечення та процесора:

  1. Наприкінці конструктора JVM виконує операцію Freeze Action (бар’єр пам’яті StoreStore / LoadStore).
  2. Це гарантує, що запис значень усіх полів у пам’ять фізично завершиться до того, як посилання на новий об’єкт Record стане видимим іншим потокам.
  3. Будь-який потік, що прочитав посилання на Record (навіть якщо публікація відбулася через стан гонитви / data race без ключового слова volatile або блокування synchronized), гарантовано побачить коректно ініціалізовані значення полів, а не їхні дефолтні значення (null, 0, false).

Повне блокування Reflection API (JEP 395)

У звичайних класах Java розробники та фреймворки десятиліттями обходили модифікатор final за допомогою рефлексії:

// Для звичайного класу (в Java 8/11):
Field field = NormalClass.class.getDeclaredField("name");
field.setAccessible(true);
field.set(instance, "Hacked"); // Працювало!

У Record ця вразливість повністю усунена на рівні віртуальної машини:

  • Метод Field.set(...) перевіряє: if (this.clazz.isRecord()) throw new IllegalAccessException(...).
  • Спроба модифікувати поле Record через рефлексію викине виняток IllegalAccessException: Can not set final field of a record class, навіть якщо попередньо було викликано field.setAccessible(true).

4 Tricky Questions

1. Якщо поля Record є final, чому об’єкт record Book(String title, List<String> authors) може змінити свій стан після створення?

Відповідь: Тому що модифікатор final забезпечує виключно поверхневу незмінність (Shallow Immutability).

Він забороняє переприсвоєння змінної посилання authors на інший об’єкт у пам’яті, але ніяк не обмежує мутацію внутрішнього стану самого списку (виклики .add(), .remove(), .clear()). Якщо при створенні в конструктор було передано змінний ArrayList, стан запису перестає бути незмінним. Щоб зробити об’єкт глибоко незмінним (Deeply Immutable), необхідно застосовувати List.copyOf() у компактному конструкторі.

2. Які гарантії Моделі пам’яті Java (JMM) надає той факт, що всі поля Record є final?

Відповідь: Відповідно до JLS §17.5 (Final Field Semantics), компілятор і процесор вставляють бар’єр пам’яті (Memory Barrier) наприкінці конструктора об’єкта.

Це забезпечує безпечну публікацію (Safe Publication): будь-який інший потік, що отримав посилання на екземпляр Record, гарантовано побачить актуальні, повністю ініціалізовані значення всіх його компонентів без необхідності використання volatile, атоміків або синхронізації synchronized. Проблема «частково сконструйованих об’єктів» для Records виключена.

3. Що станеться, якщо спробувати змінити поле Record через Reflection API (field.setAccessible(true); field.set(...))?

Відповідь: Віртуальна машина викине виняток IllegalAccessException: Can not set final field of a record class.

У специфікації Java 16+ реалізація методу Field.set() містить жорстку системну перевірку належності класу до записів (clazz.isRecord()). На відміну від звичайних класів, де рефлексивна зміна final-полів у деяких ситуаціях спрацьовувала (з попередженнями), для полів Record модифікація через Reflection API заблокована безумовно.

4. Якщо компонентом Record є масив int[], чи достатньо зробити clone() тільки в компактному конструкторі?

Відповідь: Ні, недостатньо.

Якщо клонувати масив тільки в компактному конструкторі, ви захистите Record від мутацій через посилання, передане при створенні. Проте канонічний аксесор record.array() поверне пряме посилання на внутрішній масив! Викликаючий код зможе виконати record.array()[0] = 999; і зруйнувати стан об’єкта.

Для повноцінного захисту масивів необхідно робити захисну копію у двох місцях: клонувати масив при вході в конструкторі (array = array.clone()) і клонувати його при виході, явно перевизначивши аксесор (public int[] array() { return array.clone(); }).


🎯 Шпаргалка для інтерв’ю

Головне про фінальність полів Record

  • Статус полів: Усі поля неявно є private final. Прибрати final неможливо.
  • Рівень незмінності: За замовчуванням Record гарантує лише поверхневу незмінність (Shallow Immutability).
  • Масиви та колекції: Обов’язково вимагають захисного копіювання (List.copyOf() або .clone()), інакше незмінність буде порушена.
  • JMM Safe Publication: Семантика final гарантує, що всі потоки побачать ініціалізовані поля без використання volatile.
  • Рефлексія: Поля Record неможливо змінити через Field.set(), навіть із setAccessible(true) (викидається IllegalAccessException).

Антипатерни

  • ❌ Передача сирого мутабельного списку без List.copyOf: ламає незмінність об’єкта.
  • ❌ Повернення внутрішнього масиву напряму з аксесора: дозволяє мутувати дані ззовні.
  • ❌ Надія на те, що final робить незмінним будь-який вкладений об’єкт: final фіксує лише посилання!

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