Что даёт неизменяемость
Неизменяемый (immutable) объект — тот, чьё наблюдаемое состояние не меняется после создания. Это не самоцель, а мощный инструмент простоты и безопасности.
Плюсы: потокобезопасность без всякой синхронизации (нечего портить гонкой), безопасность как ключей Map и элементов Set (хеш не «уедет»), свободное кэширование и шаринг одного экземпляра, и просто более лёгкие рассуждения о коде — значение нельзя поменять «из-под ног». Поэтому общий совет — делать классы неизменяемыми по умолчанию, а изменяемость вводить только при необходимости.
Рецепт immutable-класса
Чтобы класс был по-настоящему неизменяемым: (1) не давать сеттеров и иных мутаторов; (2) все поля сделать private final; (3) запретить наследование (класс final или приватный конструктор + фабрики), иначе подкласс может добавить изменяемость; (4) если поля ссылаются на изменяемые объекты — делать защитные копии на входе и выходе; (5) не давать ссылке this утечь из конструктора (см. безопасную публикацию).
Пункты 1–3 очевидны, а 4 — самый забываемый и важный.
Защитные копии
Если конструктор просто сохранит переданную ссылку на изменяемый объект (List, массив, Date), вызывающий сохранит свою копию ссылки и сможет поменять «внутренности» вашего объекта снаружи — неизменяемость иллюзорна. Поэтому изменяемые входные данные копируют в конструкторе, а в геттере возвращают копию или неизменяемое представление.
Это и есть defensive copy — базовый приём дизайна immutable-классов и защиты инвариантов вообще.
final class Period {
private final List<Event> events;
Period(List<Event> events) {
this.events = List.copyOf(events); // копия на входе — снаружи не подменят
}
List<Event> events() { return events; } // уже неизменяемый список
}
Безопасная публикация
Неизменяемость и многопоточность связаны напрямую: объект, у которого ВСЕ поля final и ссылка this не утекла из конструктора, по модели памяти Java безопасно публикуется — любой поток, получивший ссылку на готовый объект, увидит его поля корректно установленными без синхронизации (final field freeze).
Именно поэтому «immutable ⇒ потокобезопасен». Подробно про этот механизм — в уроке про JMM.
Стоимость и приёмы
Цена неизменяемости — копирование при «изменении»: вместо мутации создают новый объект (методы withX, возвращающие копию с одним изменённым полем). Для редких изменений это дёшево; для частых — используют изменяемый Builder, который собирает состояние, а на выходе отдаёт неизменяемый результат, или специализированные persistent-структуры.
Главное — не превращать «withX на каждый чих» в источник мусора в горячем цикле; там уместен мутабельный буфер с финальной заморозкой.
records
Современный лаконичный способ объявить неизменяемый носитель данных — record (см. урок про современную Java): он автоматически делает компоненты private final, генерирует конструктор, геттеры, equals/hashCode и toString.
Важная оговорка: record гарантирует неизменяемость только своих ССЫЛОК, но не делает защитных копий за вас. Если компонент — изменяемый List, его всё равно нужно копировать в компактном конструкторе, иначе «неизменяемая» запись протекает.
Как отвечать на собеседовании
Назовите выгоды (потокобезопасность, безопасные ключи, кэшируемость) и рецепт (final-класс, private final поля, без сеттеров, защитные копии, не публиковать this). Подчеркните defensive copy на входе/выходе как самый забываемый пункт, свяжите immutable с безопасной публикацией (все поля final → потокобезопасен) и оговорите, что record не делает защитных копий сам.