Являются ли поля 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фиксирует только ссылку!