Какие преимущества у неизменяемых классов по сравнению с изменяемыми в Java
Что проверяют
Понимаете ли вы, откуда у неизменяемых объектов берутся потокобезопасность и надёжность, и умеете ли написать такой класс без дыр. Следом почти всегда дают класс с полем List или Date и просят найти, где неизменяемость ломается.
Как отвечать
Преимущества:
- потокобезопасность без блокировок: состояние не меняется, поэтому гонок нет. Модель памяти Java отдельно гарантирует, что
final-поля, записанные в конструкторе, видны другим потокам без синхронизации, если ссылка наthisне утекла из конструктора; - безопасные ключи: объект можно класть в
HashMapиHashSet, егоhashCodeне изменится, поэтому его можно закешировать, как делаетString; - свободное совместное использование: один экземпляр раздают всем без защитных копий. На этом построены пул строк и кеш
Integer.valueOfдля значений от -128 до 127; - атомарность отказа: метод, упавший с исключением, не оставит объект в полуизменённом состоянии, потому что менять нечего;
- простота рассуждений: инвариант проверяется один раз в конструкторе и дальше держится всегда.
Рецепт класса: final на классе или приватный конструктор с фабрикой, все поля private final, никаких сеттеров, методы изменения возвращают новый объект, изменяемые аргументы копируются на входе (List.copyOf) и не отдаются наружу как есть. С Java 16 для носителей данных есть record, но его неизменяемость поверхностная.
Цена: каждое изменение порождает новый объект, это аллокации и работа для GC. Для горячих циклов берут изменяемый помощник, как StringBuilder для String, а для классов с десятком полей Builder.
Где спотыкаются
Считают поле final List<String> неизменяемым. Неизменяема ссылка, а содержимое можно менять снаружи, если список пришёл в конструктор без копии или отдаётся геттером.
Используют Collections.unmodifiableList(list) вместо копии. Это лишь обёртка над исходным списком: его изменения через неё видны.
Забывают про final на классе. Наследник может добавить изменяемое состояние или переопределить методы, и гарантии для кода, который принимает базовый тип, пропадут.