The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:
В объектно-ориентированном программировании объект считается неизменяемым, если его состояние не может быть изменено после создания. В Java хорошим примером неизменяемого объекта является String
. После создания его состояние нельзя изменить. Мы можем запросить создание новых строк, но его собственное состояние никогда не изменится.
Однако в JDK не так много неизменяемых классов. Возьмем, например, класс Date
. Его состояние можно изменить с помощью метода setTime()
.
Не знаю, почему разработчики JDK решили сделать эти два очень похожих класса по-разному. Однако я считаю, что дизайн изменяемого Date
имеет много недостатков, в то время как неизменяемый String
более соответствует объектно-ориентированной парадигме.
Более того, я считаю, что в идеальном объектно-ориентированном мире все классы должны быть неизменяемыми. К сожалению, иногда это технически невозможно из-за ограничений в JVM. Тем не менее, мы всегда должны стремиться к лучшему.
Это неполный список аргументов в пользу неизменяемости:
истинно неизменяемые объекты всегда являются потокобезопасными
они помогают избежать временной связности
их использование не оказывает побочных эффектов (без защитных копий)
проблема изменяемости идентичности избегается
они всегда имеют атомарность сбоя
они намного проще кэшировать
они предотвращают NULL ссылки, которые являются плохими
Давайте обсудим наиболее важные аргументы по одному.
Первый и наиболее очевидный аргумент заключается в том, что неизменяемые объекты являются потокобезопасными. Это означает, что несколько потоков могут получать доступ к одному и тому же объекту одновременно, не конфликтуя с другим потоком.
Если методы объекта не могут изменять его состояние, независимо от того, сколько из них и как часто они вызываются параллельно, они будут работать в своем собственном пространстве памяти в стеке.
Гоэтц и др. подробно объяснили преимущества неизменяемых объектов в своей очень известной книге “Java Concurrency in Practice” (настоятельно рекомендуется).
Вот пример временной связи (в коде выполняются два последовательных HTTP POST-запроса, где второй запрос содержит тело HTTP).
Этот код работает. Однако, следует помнить, что первый запрос должен быть настроен до того, как может произойти второй запрос. Если мы решим удалить первый запрос из скрипта, мы также удалим вторую и третью строку, и не получим никаких ошибок от компилятора.
Теперь скрипт сломан, хотя он компилировался без ошибок. В этом и заключается временная связность - в коде всегда есть скрытая информация, которую программист должен помнить. В этом примере мы должны помнить, что конфигурация для первого запроса также используется для второго.
Мы должны помнить, что второй запрос всегда должен оставаться вместе и выполняться после первого.
Если бы класс Request
был неизменяемым, первый отрывок вообще не работал бы и был бы переписан так:
Теперь эти два запроса не связаны друг с другом. Мы можем безопасно удалить первый, и второй всё равно будет работать правильно. Вы можете указать на наличие дублирования кода. Да, мы должны избавиться от него и переписать код:
Видите ли, рефакторинг ничего не сломал, и у нас все еще нет временной связности. Первый запрос может быть безопасно удален из кода, не затрагивая второй.
Я надеюсь, что этот пример показывает, что код, работающий с неизменяемыми объектами, более читабелен и поддерживаем, потому что он не имеет временной связности.
Давайте попробуем использовать наш класс Request
в новом методе (теперь он изменяемый):
Давайте попробуем выполнить два запроса — первый с методом GET и второй с методом POST:
Метод post()
имеет “побочный эффект” - он вносит изменения в изменяемый объект request
. В данном случае такие изменения не ожидаются. Мы ожидаем, что он выполнит POST-запрос и вернет его тело. Мы не хотим читать его документацию только для того, чтобы узнать, что за кулисами он также изменяет передаваемый ему в качестве аргумента запрос.
Само собой разумеется, такие побочные эффекты приводят к ошибкам и проблемам с поддержкой. Было бы намного лучше работать с неизменяемым Request
.
В данном случае у нас не может быть никаких побочных эффектов. Никто не может изменить наш объект request
, независимо от того, где он используется и насколько глубоко передается через стек вызовов методов.
Этот код абсолютно безопасен и не вызывает побочных эффектов.
Очень часто мы хотим, чтобы объекты были идентичными, если их внутренние состояния совпадают. Класс Date
является хорошим примером:
Есть два разных объекта; однако они равны друг другу, потому что их инкапсулированные состояния одинаковы. Это становится возможным благодаря их настраиваемой перегруженной реализации методов equals()
и hashCode()
.
Последствием такого удобного подхода, используемого с изменяемыми объектами, является то, что каждый раз при изменении состояния объекта меняется его идентичность.
Это может выглядеть естественно, пока вы не начнете использовать изменяемые объекты в качестве ключей в картах:
При изменении состояния объекта date
мы не ожидаем, что он изменит свою идентичность. Мы не ожидаем потери записи в карте только потому, что изменено состояние ее ключа. Однако именно это происходит в приведенном выше примере.
Когда мы добавляем объект в карту, его hashCode()
возвращает одно значение. Это значение используется HashMap
для размещения записи во внутренней хэш-таблице. Когда мы вызываем containsKey()
, хэш-код объекта отличается (поскольку он основан на его внутреннем состоянии), и HashMap
не может найти его во внутренней хэш-таблице.
Это очень раздражающая и сложная для отладки побочная эффекты изменяемых объектов. Неизменяемые объекты полностью избегают этого.
Вот простой пример:
Очевидно, что объект класса Stack
останется в поврежденном состоянии, если он вызывает исключение времени выполнения при переполнении. Его свойство size
будет увеличиваться, в то время как items
не получит новый элемент.
Неизменяемость предотвращает эту проблему. Объект никогда не будет оставаться в поврежденном состоянии, потому что его состояние изменяется только в его конструкторе. Конструктор либо завершится с ошибкой, отклоняя создание объекта, либо успешно завершится, создавая действительный неподвижный объект, которые никогда не изменяет свое инкапсулированное состояние.
Для получения дополнительной информации по этой теме прочитайте Effective Java от Joshua Bloch.
Существует несколько аргументов против неизменности.
- «Обновление существующего объекта обходится дешевле, чем создание нового». Компания Oracle считает, что «Влияние создания объекта часто переоценивается и может быть компенсировано некоторой эффективностью, связанной с неизменяемыми объектами. К ним относятся уменьшение накладных расходов, связанных с сборкой мусора, и устранение необходимости кода для защиты изменяемых объектов от повреждений». Я согласен.
Если у вас есть еще какие-то аргументы, пожалуйста, оставьте их ниже, и я постараюсь прокомментировать.
P.S. Проверьте takes.org, Java веб-фреймворк, состоящий полностью из неизменяемых объектов.
Translated by ChatGPT gpt-3.5-turbo/42 on 2023-11-28 at 15:21