Если прочитать
определение immutable-объекта внимательно, момент его создания – это единственный этап в жизненном цикле, когда объект может меняться. Действительно, как иначе установить это неизменяемое состояние. Отсюда, при неправильной реализации, вытекают некоторые риски дальнейшей неизменяемости.
Первое, самое очевидное правило: копируйте поля ссылочных типов. Когда пользователь передал в конструктор изменяемый объект, возможно он все еще владеет ссылкой на него. Тогда, пользуясь этой ссылкой, пользователь может менять поле вашего иммутабельного объекта позднее. Создав копию объекта-параметра перед присвоением в поле, вы получите эксклюзивную ссылку, недоступную извне.
Вторая проблема, с которой вы гораздо менее вероятно столкнетесь, но о которой нужно знать: без должной синхронизации в многопоточной среде конструктор может получить ссылку на не до конца сконструированный объект. Для поддержки создания объектов в многопоточной среде нужно обеспечить создание его полей
happens-before. Проблема аналогична double-checked блокировке без ключевого слова
volatile
– детали можно почитать
здесь.
Что касается удобства создания immutable объектов. Если класс состоит из всего пары полей – дополнительные действия скорее всего не нужны, хватит обычного конструктора.
Однако, когда полей много, это
становится проблемой. В Java, в отличие от таких языков как например TypeScript или Kotlin, нельзя указывать
имена передаваемых параметров. Обычно можно воспользоваться сеттерами, но в неизменяемом классе их нет.
Похожая проблема возникает, когда процесс создания объекта не может быть атомарным, потому что он разделен между несколькими сущностями. Разные компоненты предоставляют разные поля, приходится копить их во временных переменных, прежде чем вызвать конструктор.
На помощь приходят
порождающие паттерны. Самый распространенный подход – builder (паттерн Строитель). Для иммутабельного типа
Foo
создается дополнительный
изменяемый класс
FooBuilder
. В билдере есть сеттеры для каждого поля. Эти сеттеры обычно возвращают
this
, что позволяет вызывать их
в цепочке. Цепочка заканчивается методом
build()
, который уже создает иммутабельный
Foo
.
Каждый сеттер выполняет роль именованного параметра. Сам билдер, как обычный объект, можно передавать от компонента к компоненту, делегируя им части инициализации. Использование этого паттерна логически разделяет существование объекта типа
Foo
на два этапа – создание и использование.