🔒 Раздел 13 · Вопрос #8

Достаточно ли сделать все поля final для иммутабельности

Модификатор final на уровне поля гарантирует только одно: саму ссылку нельзя перенаправить на другой объект после инициализации. Однако final никак не защищает внутреннее состоя...


🟢 Junior Level

30-секундный ответ

Категорически нет, недостаточно! Это одно из самых распространённых заблуждений на собеседованиях.

Модификатор final на уровне поля гарантирует только одно: саму ссылку нельзя перенаправить на другой объект после инициализации. Однако final никак не защищает внутреннее состояние объекта, на который указывает эта ссылка:

  • Если поле ссылается на мутабельный объект (например, ArrayList, HashMap, массив int[] или java.util.Date), данные внутри этого объекта можно свободно менять через методы-мутаторы.
  • Если класс не объявлен как final, класс-наследник может переопределить геттеры или добавить мутабельное состояние.
  • Если ссылка this утекает из конструктора, другие потоки увидят неинициализированные поля.

Наглядный пример уязвимости

// Класс выглядит "иммутабельным", но на самом деле он МУТАБЕЛЕН!
public final class VulnerableUser {
    private final String name;
    private final List<String> roles; // final ссылка!

    public VulnerableUser(String name, List<String> roles) {
        this.name = name;
        this.roles = roles; // ОШИБКА 1: сохранена прямая ссылка на внешний список
    }

    public String getName() { return name; }

    public List<String> getRoles() {
        return roles; // ОШИБКА 2: возвращена прямая ссылка на внутренний список
    }
}

Как внешний код легко ломает этот класс:

List<String> myRoles = new ArrayList<>();
myRoles.add("USER");

VulnerableUser user = new VulnerableUser("Alice", myRoles);

// Взлом 1: модификация через исходный список
myRoles.add("ADMIN"); 
System.out.println(user.getRoles()); // [USER, ADMIN] — состояние изменилось извне!

// Взлом 2: модификация через геттер
user.getRoles().clear();
System.out.println(user.getRoles()); // [] — состояние очищено!

🟡 Middle Level

Поверхностная (Shallow) vs Глубокая (Deep) иммутабельность

  1. Shallow Immutability (Поверхностная): поля объекта объявлены как final, но объекты, на которые они ссылаются, мутабельны. Пример: record User(String name, List<String> roles).
  2. Deep Immutability (Глубокая): состояние всего транзитивного графа объектов неизменно от корня до листовых узлов.
Поверхностная иммутабельность:
[User Object] (final ref) ───► [ArrayList] (МУТАБЕЛЕН: size, elementData[])

Глубокая иммутабельность:
[User Object] (final ref) ───► [ImmutableCollections.ListN] (неизменяем) ───► ["ADMIN"] (String, неизменяем)

Когда модификатора final ДЕЙСТВИТЕЛЬНО достаточно

Объявление всех полей private final в связке с final классом делает объект истинно иммутабельным только тогда, когда все поля принадлежат к следующим категориям:

  1. Примитивные типы: int, long, boolean, double и т.д.
  2. Известные иммутабельные классы стандартной библиотеки:
    • Строки: java.lang.String.
    • Обёртки примитивов: Integer, Long, Double и т.д.
    • Числа произвольной точности: BigDecimal, BigInteger.
    • Дата и время (Java 8+): LocalDate, LocalTime, Instant, Duration.
    • Идентификаторы: UUID.
    • Другие глубоко иммутабельные пользовательские классы.

Когда модификатора final КАТЕГОРИЧЕСКИ недостаточно

Если класс содержит:

  • Массивы: любые массивы в Java (int[], String[]) всегда мутабельны.
  • Коллекции: List, Set, Map (даже если поле final, методы add, remove, put изменяют данные).
  • Устаревшие классы даты/времени: java.util.Date, java.util.Calendar (содержат методы setTime(), setYear()).
  • Любые мутабельные доменные объекты: классы с сеттерами или изменяемым внутренним состоянием.

🔴 Senior Level

Полная формула истинной иммутабельности

Истинная иммутабельность — это свойство всего графа объектов и процесса их инициализации: \(\text{True Immutability} = \text{final class} + \text{private final fields} + \text{Defensive Copy (In/Out)} + \text{No This-Escape}\)

Каждый компонент формулы закрывает отдельный вектор атаки:

  1. final class: закрывает вектор подмены поведения через полиморфизм и переопределение геттеров.
  2. private final fields: закрывает прямой доступ к памяти и запускает Freeze Action JMM.
  3. Defensive Copy: изолирует внутреннее состояние от внешних мутаций через входные аргументы или возвращаемые геттеры.
  4. No This-Escape: гарантирует, что ссылка на объект не будет прочитана другим потоком до завершения действия Freeze Action.

Границы гарантий JMM §17.5: Транзитивность Freeze Action

Критический нюанс модели памяти Java, о котором часто забывают даже Senior-разработчики:

  • Действие Freeze Action в конце конструктора «замораживает» только само ссылочное поле.
  • JMM не гарантирует транзитивную заморозку внутреннего состояния объекта, на который ссылается final-поле, если этот объект был создан вне конструктора!
  • Если вы передали в конструктор уже существующий мутабельный ArrayList и сохранили его в final-поле:
    • Ссылка на ArrayList опубликована безопасно.
    • Однако элементы внутри ArrayList могут модифицироваться параллельным потоком без синхронизации, порождая классическую гонку данных (Data Race) по правилам JMM.

Логическая (Observational) vs Битовая (Bitwise) иммутабельность

В архитектуре высокопроизводительных систем разделяют:

  • Битовая иммутабельность (Bitwise): ни один бит оперативной памяти объекта не меняется после выхода из конструктора.
  • Логическая иммутабельность (Observational): с точки зрения внешнего наблюдателя состояние и поведение объекта неизменны, но внутренние служебные поля могут мутировать.

Классический пример логической иммутабельности — класс java.lang.String:

public final class String {
    private final byte[] value; // Битово иммутабелен
    private int hash;           // НЕ final! Мутирует при первом вызове hashCode()
}

Строка не является битово иммутабельной, но остаётся абсолютно потокобезопасной и логически неизменяемой.


4 Tricky Questions

1. Распространяется ли действие JMM §17.5 Freeze Action транзитивно на объекты, доступные по цепочке ссылок из final-поля?

Ответ: Спецификация JMM §17.5 явно определяет: гарантии безопасной публикации распространяются на объект и транзитивно на все объекты, достижимые через его final-поля, только если эти зависимые объекты были созданы и инициализированы в рамках конструирования основного объекта до момента Freeze Action. Если же в final-поле сохраняется ссылка на объект, созданный где-то снаружи (например, переданный параметром в конструктор), то никакой транзитивной гарантии на его поля нет! Если внешний код продолжит менять переданный объект, читатели увидят состояние гонки. Именно поэтому защитное копирование (List.copyOf(), clone()) внутри конструктора абсолютно обязательно для обеспечения гарантий многопоточности JMM.


2. Является ли Java record глубоко иммутабельным «из коробки», если один из его компонентов — массив байтов byte[]?

Ответ: Нет. Java record обеспечивает исключительно поверхностную иммутабельность (Shallow Immutability):

  1. Компилятор объявляет класс как final и все компоненты как private final.
  2. Однако для компонента byte[] сгенерированный канонический конструктор просто скопирует ссылку на массив, а геттер componentName() вернёт прямую ссылку на этот массив.
  3. Любой клиент сможет написать recordInstance.data()[0] = 0xFF; и изменить состояние записи. Чтобы сделать Record глубоко иммутабельным, разработчик обязан вручную переопределить компактный конструктор с вызовом .clone() и геттер с возвратом .clone().

3. Если класс объявлен как final, все поля private final, но геттер возвращает Collections.unmodifiableList(this.internalList), может ли состояние объекта измениться?

Ответ: Да, может, если internalList был сохранён в конструкторе напрямую без защитной копии! Collections.unmodifiableList() — это не копия, а всего лишь представление (view) поверх существующего списка. Обертка защищает от вызова методов мутации через сам геттер (list.add() бросит UnsupportedOperationException). Но если тот, кто создавал объект, сохранил ссылку на исходный ArrayList, он может в любой момент вызвать originalList.add("Boom"), и это изменение мгновенно отразится внутри «иммутабельного» объекта и в возвращаемом unmodifiableList.


4. В чём опасность использования Cloneable / clone() для создания защитной копии полей в иммутабельном классе?

Ответ: Метод clone() в Java архитектурно сломан (Effective Java, Item 13):

  1. Если поле имеет нефинальный тип (например, CustomClass), то переданный в конструктор объект может оказаться вредоносным подклассом MaliciousCustomClass extends CustomClass.
  2. Вызов parameter.clone() вызовет переопределённый метод подкласса, который может вернуть вовсе не независимую копию, а сохранить ссылку на объект внутри хакерского реестра.
  3. Кроме того, Object.clone() выполняет лишь поверхностное копирование (shallow copy). Вывод: Использовать clone() безопасно только для массивов (так как тип массива точен и не имеет подклассов: array.clone()). Для всех остальных классов необходимо использовать конструкторы копирования или статические фабричные методы.

🎯 Шпаргалка для интервью

Сравнение: Только final поля vs Истинно иммутабельный класс

| Критерий | Только final поля | Истинно иммутабельный класс | | :— | :— | :— | | Переприсвоение ссылки | Запрещено компилятором | Запрещено компилятором | | Мутация содержимого полей | ❌ Возможно (коллекции, массивы) | ✅ Исключено (defensive copy) | | Защита от подклассов | ❌ Нет (если класс не final) | ✅ Запрещено (final class) | | Safe Publication в JMM | Частично (только сами ссылки) | Полная транзитивная безопасность | | Модификация через геттеры | ❌ Возможно, если вернуть мутабель | ✅ Возврат unmodifiable / копий |

Красные флаги на собеседовании (Чего говорить нельзя)

  • ❌ «Да, модификатора final на полях абсолютно достаточно для иммутабельности» — фатальный ответ, дисквалифицирующий кандидата.
  • ❌ «Java Record всегда глубоко иммутабелен» — Records обеспечивают только shallow immutability; коллекции и массивы требуют ручной защиты.
  • ❌ «Collections.unmodifiableList() создаёт независимую копию списка» — это обёртка-view, исходный список остаётся связанным.
  • ❌ «Для глубокой иммутабельности достаточно скопировать коллекцию через new ArrayList<>(input)» — это скопирует только ссылки; если элементы списка мутабельны, их тоже нужно клонировать.

Связанные вопросы