Вопрос 16 · Раздел 13

Почему иммутабельный класс должен быть final?

Иммутабельный класс должен быть final, чтобы никто не мог создать его подкласс и добавить изменяемые поля или методы.

Версии по языкам: English Russian Ukrainian

Junior Level

Иммутабельный класс должен быть final, чтобы никто не мог создать его подкласс и добавить изменяемые поля или методы.

public final class Point {  // final — нельзя наследоваться
    private final int x;
    private final int y;
    // ...
}

Если убрать final, кто-то может написать:

public class MutablePoint extends Point {
    private int z; // новое поле
    public void setZ(int z) { this.z = z; } // мутация!
}

Middle Level

Защита от подмены поведения

Без final подкласс может:

  1. Добавить мутабельное состояние — новые поля с сеттерами
  2. Переопределить геттеры — возвращать другие данные
  3. Переопределить equals/hashCode — сломать работу в HashMap

Сценарий атаки

public void process(ImmutablePoint point) {
    validate(point);         // проверяем координаты
    // здесь подкласс может изменить данные
    saveToDatabase(point);   // сохраняем уже изменённые данные
}

Примеры из JDK

public final class String { ... }
public final class Integer { ... }
public final class BigDecimal { ... }
public final class LocalDate { ... }

Что если нужно наследование?

Используйте Sealed classes (Java 17+) для контроля наследников, или композицию.

Или использовать sealed классы (Java 17+), которые ограничивают круг наследников.

Когда final может мешать

  • Mocking в тестах: Mockito не может моковать final-классы без bytebuddy-агента
  • Прокси-фреймворки: некоторые требуют наследования (Spring AOP через CGLIB)
  • Решение: sealed classes или Mockito inline-mock-maker

Senior Level

Гарантии JMM и JIT

final class + final fields создаёт “непробиваемую” оболочку:

  • JMM: final поля обеспечивают safe publication с memory barrier
  • JIT: компилятор может агрессивно инлайнить методы и кэшировать значения, зная что класс не будет переопределён

Проблемы без final

  1. HashMap corruption — подкласс может переопределить hashCode(), возвращая разные значения
  2. Security vulnerabilities — TOCTOU-атаки через подмену результата геттера
  3. Thread-safety loss — подкласс обращается к не-final полям без синхронизации

Резюме для Senior

  • final класс предотвращает изменение логики через переопределение методов
  • Без final нельзя гарантировать потокобезопасность и стабильность хеш-кода
  • final class — сигнал другим разработчикам и компилятору о неизменном контракте
  • Для контролируемого наследования используйте sealed классы (Java 17+)

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

Обязательно знать:

  • final запрещает наследование — подкласс не добавит мутабельность
  • Без final подкласс может: добавить mutable поля, переопределить геттеры, сломать equals/hashCode
  • HashMap corruption — подкласс переопределяет hashCode(), объект теряется
  • Security vulnerabilities — TOCTOU-атаки через подмену результата геттера
  • JMM: final class + final fields = непробиваемая оболочка с safe publication
  • Когда final мешает: mocking в тестах, прокси-фреймворки (CGLIB); решение: sealed classes или inline-mock-maker

Частые уточняющие вопросы:

  • Что будет без final? — Подкласс добавит mutable поля, сломает hashCode и thread-safety
  • Mockito не может мокать final-классы? — Нужен inline-mock-maker или bytebuddy-агент
  • Sealed classes как альтернатива? — Да, Java 17+: ограничивают круг наследников
  • JIT выигрывает от final? — Да: агрессивный inlining, кэширование, т.к. класс не переопределят

Красные флаги (НЕ говорить):

  • «Без final иммутабельность работает» — подкласс может всё сломать
  • «Final нужен только для стиля» — это гарантия безопасности и JIT-оптимизаций
  • «Mockito не работает с final» — работает с inline-mock-maker
  • «Sealed classes = final» — sealed разрешает наследование от конкретных классов

Связанные темы:

  • [[7. Что такое ключевое слово final и как оно помогает в создании иммутабельных классов]]
  • [[15. Можно ли наследоваться от иммутабельного класса]]
  • [[17. Что произойдёт, если переопределить геттер в подклассе иммутабельного класса]]
  • [[1. Что такое иммутабельный (неизменяемый) объект]]