Чи є поля 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):
- Для колекцій: використовуйте
List.copyOf(),Set.copyOf(),Map.copyOf()у компактному конструкторі:public record ShoppingCart(String user, List<String> items) { public ShoppingCart { // List.copyOf повертає суворо незмінний список і захищає від зовнішніх мутацій: items = List.copyOf(items); } } - Для масивів: потрібне захисне копіювання і в конструкторі, і в аксесорі:
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).
Як це працює на рівні апаратного забезпечення та процесора:
- Наприкінці конструктора JVM виконує операцію Freeze Action (бар’єр пам’яті
StoreStore/LoadStore). - Це гарантує, що запис значень усіх полів у пам’ять фізично завершиться до того, як посилання на новий об’єкт
Recordстане видимим іншим потокам. - Будь-який потік, що прочитав посилання на
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фіксує лише посилання!