Зачем нам неизменный класс?


84

Я не могу понять, в каких сценариях нам нужен неизменяемый класс.
Вы когда-нибудь сталкивались с подобным требованием? или не могли бы вы привести нам какой-нибудь реальный пример, где мы должны использовать этот шаблон.


13
Я удивлен, что никто не назвал это дубликатом. Похоже, вы, ребята, просто пытаетесь набрать легкие очки ... :) (1) stackoverflow.com/questions/1284727/mutable-or-immutable-class (2) stackoverflow.com/questions/3162665/immutable-class ( 3) stackoverflow.com/questions/752280/…
Джавид Джамае,

Ответы:


82

Другие ответы кажутся слишком сосредоточенными на объяснении, почему неизменность - это хорошо. Это очень хорошо, и я использую его по возможности. Однако это не ваш вопрос . Я отвечу на ваш вопрос по пунктам, чтобы убедиться, что вы получаете ответы и примеры, которые вам нужны.

Я не могу понять, в каких сценариях нам нужен неизменяемый класс.

«Потребность» здесь относительный термин. Неизменяемые классы - это шаблон проектирования, который, как и любая парадигма / шаблон / инструмент, предназначен для упрощения создания программного обеспечения. Точно так же много кода было написано до появления парадигмы объектно-ориентированного программирования, но считайте меня одним из программистов, которым "нужен" объектно-ориентированный объект. Неизменяемые классы, такие как OO, не нужны строго , но я буду действовать так, как будто они мне нужны.

Вы когда-нибудь сталкивались с подобным требованием?

Если вы не смотрите на объекты в проблемной области с правильной точки зрения, вы можете не увидеть требования для неизменяемого объекта. Может быть легко подумать, что проблемная область не требует никаких неизменяемых классов, если вы не знаете, когда их использовать с пользой.

Я часто использую неизменяемые классы, когда думаю о данном объекте в моей проблемной области как о значении или фиксированном экземпляре . Это понятие иногда зависит от перспективы или точки зрения, но в идеале будет легко переключиться на правильную перспективу, чтобы определить хорошие объекты-кандидаты.

Вы можете лучше понять, где неизменяемые объекты действительно полезны (если не строго необходимы), прочитав различные книги / онлайн-статьи, чтобы развить хорошее представление о том, как думать о неизменяемых классах. Хорошая статья для начала - теория и практика Java: мутировать или не мутировать?

Я постараюсь привести ниже несколько примеров того, как можно видеть объекты в разных ракурсах (изменяемые и неизменные), чтобы прояснить, что я имею в виду под перспективой.

... не могли бы вы привести нам какой-нибудь реальный пример, где мы должны использовать этот шаблон.

Поскольку вы просили реальных примеров, я дам вам несколько, но сначала давайте начнем с классических примеров.

Классические объекты-ценности

Строки и целые числа часто рассматриваются как значения. Поэтому неудивительно, что класс String и класс-оболочка Integer (а также другие классы-оболочки) неизменяемы в Java. Цвет обычно рассматривается как значение, отсюда неизменный класс Color.

Контрпример

Напротив, автомобиль обычно не рассматривается как объект ценности. Моделирование автомобиля обычно означает создание класса с изменяющимся состоянием (одометр, скорость, уровень топлива и т. Д.). Однако есть некоторые области, в которых car может быть объектом значения. Например, автомобиль (или, в частности, модель автомобиля) можно рассматривать как объект значения в приложении для поиска подходящего моторного масла для данного автомобиля.

Играя в карты

Вы когда-нибудь писали программу для игры в карты? Я сделал. Я мог бы представить игральную карту как изменяемый объект с изменяемой мастью и рангом. Рука с дро-покером может быть 5 фиксированных случаев, когда замена 5-й карты в моей руке будет означать преобразование 5-го экземпляра игральной карты в новую карту путем изменения ее масти и ранга ivars.

Однако я склонен думать об игральной карте как о неизменяемом объекте, который после создания имеет фиксированную неизменную масть и ранг. Моя комбинация дро-покера будет 5 экземпляров, и замена карты в моей руке потребует сброса одного из этих экземпляров и добавления нового случайного экземпляра в мою руку.

Проекция карты

Последний пример - когда я работал над кодом карты, где карта могла отображаться в различных проекциях . В исходном коде карта использовала фиксированный, но изменяемый экземпляр проекции (например, изменяемая игральная карта выше). Изменение проекции карты означало изменение ivars экземпляра проекции карты (тип проекции, центральная точка, масштабирование и т. Д.).

Однако я чувствовал, что дизайн был бы проще, если бы я думал о проекции как о неизменяемом значении или фиксированном экземпляре. Изменение проекции карты означало, что карта будет ссылаться на другой экземпляр проекции, а не изменять фиксированный экземпляр проекции карты. Это также упростило захват именованных проекций, таких как MERCATOR_WORLD_VIEW.


42

Неизменяемые классы в целом намного проще спроектировать, реализовать и правильно использовать. . Примером является String: реализация java.lang.Stringзначительно проще, чем std::stringв C ++, в основном из-за его неизменности.

Одной конкретной областью, в которой неизменность имеет особенно большое значение, является параллелизм: неизменяемые объекты могут безопасно совместно использоваться несколькими потоками , тогда как изменяемые объекты должны быть поточно-безопасными за счет тщательного проектирования и реализации - обычно это далеко не тривиальная задача.

Обновление: эффективное Java 2nd Edition решает эту проблему подробно - см. Правило 15: Минимизируйте изменчивость .

См. Также эти связанные сообщения:


39

Эффективная Java от Джошуа Блоха описывает несколько причин для написания неизменяемых классов:

  • Простота - каждый класс находится только в одном состоянии
  • Thread Safe - поскольку состояние не может быть изменено, синхронизация не требуется
  • Написание в неизменном стиле может привести к более надежному коду. Представьте себе, если бы строки не были неизменными; Любые методы получения, возвращающие String, потребуют от реализации создания защитной копии перед возвратом String - в противном случае клиент может случайно или злонамеренно нарушить это состояние объекта.

В общем, рекомендуется делать объект неизменяемым, если в результате не возникают серьезные проблемы с производительностью. В таких обстоятельствах изменяемые объекты построителя могут использоваться для создания неизменяемых объектов, например StringBuilder.


1
Каждый класс находится в одном состоянии или каждый объект?
Тушар Такур

@TusharThakur, каждый объект.
joker

17

Хэш-карты - классический пример. Ключ к карте обязательно должен быть неизменным. Если ключ не является неизменяемым, и вы измените значение ключа так, что hashCode () приведет к новому значению, карта теперь повреждена (ключ теперь находится в неправильном месте в хеш-таблице).


3
Я бы сказал, что ключ нельзя менять; однако официального требования для его неизменности нет.
Péter Török

Не уверен, что вы имеете в виду под «официальным».
Кирк Уолл,

1
Я думаю, он имеет в виду, что единственное реальное требование - это то, что ключи НЕ ДОЛЖНЫ быть изменены ... не то, чтобы они вообще НЕ МОГУТ быть изменены (через неизменность). Конечно, самый простой способ предотвратить изменение ключей - это в первую очередь просто сделать их неизменяемыми! :-)
Platinum Azure

2
Например, download.oracle.com/javase/6/docs/api/java/util/Map.html : «Примечание: необходимо проявлять большую осторожность, если изменяемые объекты используются в качестве ключей карты». То есть изменяемые объекты могут использоваться как ключи.
Péter Török,

8

Java - это практически все без исключения ссылки. Иногда на экземпляр ссылаются несколько раз. Если вы измените такой экземпляр, он будет отражен во всех его ссылках. Иногда этого просто не нужно для повышения надежности и безопасности потоков. В этом случае полезен неизменяемый класс, так что вы будете вынуждены создать новый экземпляр и переназначить его текущей ссылке. Таким образом, исходный экземпляр других ссылок останется нетронутым.

Представьте, как бы выглядела Java, если бы она Stringбыла изменяемой.


12
Или если бы Dateи Calendarбыли изменчивыми. Ой, подождите, они такие, OH SH
gustafc

1
@gustafc: Изменяемый календарь - это нормально, учитывая его работу по вычислению дат (можно сделать, возвращая копии, но, учитывая, насколько тяжелым является Календарь, так лучше). а вот Дата - да, мерзко.
Майкл Боргвардт,

В некоторых реализациях JRE Stringможет изменяться! (подсказка: некоторые старые версии JRockit). Вызов string.trim () привел к обрезке исходной строки
Саландур,

6
@Salandur: Тогда это не «реализация JRE». Это реализация чего-то похожего на JRE, но на самом деле это не так.
Марк Питерс,

@Mark Peters: очень правдивое заявление
Саландур

6

По сути, нам не нужны неизменяемые классы, но они определенно могут упростить некоторые задачи программирования, особенно когда задействовано несколько потоков. Вам не нужно выполнять какие-либо блокировки для доступа к неизменяемому объекту, и любые факты, которые вы уже установили о таком объекте, останутся верными в будущем.


6

Возьмем крайний случай: целочисленные константы. Если я напишу такое утверждение, как «x = x + 1», я хочу быть на 100% уверенным, что число «1» каким-то образом не превратится в 2, что бы ни происходило где-либо еще в программе.

Хорошо, целочисленные константы - это не класс, но концепция та же. Допустим, я пишу:

String customerId=getCustomerId();
String customerName=getCustomerName(customerId);
String customerBalance=getCustomerBalance(customerid);

Выглядит достаточно просто. Но если бы строки не были неизменяемыми, то мне пришлось бы рассмотреть возможность того, что getCustomerName может изменить customerId, так что когда я вызываю getCustomerBalance, я получаю баланс для другого клиента. Теперь вы можете спросить: «С какой стати кто-нибудь, пишущий функцию getCustomerName, заставит ее изменить идентификатор? В этом нет смысла». Но именно здесь у вас могут возникнуть проблемы. Человек, пишущий приведенный выше код, может счесть очевидным, что функции не изменят параметр. Затем приходит кто-то, кто должен изменить другое использование этой функции для обработки случая, когда у клиента есть несколько учетных записей под одним и тем же именем. И он говорит: «О, вот эта удобная функция имени getCustomer, которая уже ищет имя. Я»

Неизменяемость просто означает, что определенный класс объектов является константами, и мы можем рассматривать их как константы.

(Конечно, пользователь может присвоить переменной другой "постоянный объект". Кто-то может написать String s = "hello"; а затем написать s = "goodbye"; если я не сделаю переменную final, я не могу быть уверен что он не изменяется в моем собственном блоке кода. Точно так же, как целочисленные константы, уверяют меня, что «1» всегда одно и то же число, но не то, что «x = 1» никогда не будет изменено записью «x = 2». Но я может быть уверенным в том, что если у меня есть дескриптор неизменяемого объекта, что никакая функция, которой я передаю его, не может изменить его на мне, или что если я сделаю две его копии, что изменение переменной, содержащей одну копию, не изменит прочее.


5

Есть разные причины неизменности:

  • Безопасность потоков: неизменяемые объекты не могут быть изменены, а их внутреннее состояние не может измениться, поэтому нет необходимости синхронизировать их.
  • Это также гарантирует, что все, что я отправляю (через сеть), должно быть в том же состоянии, что и ранее. Это означает, что никто (перехватчик) не может прийти и добавить случайные данные в мой неизменяемый набор.
  • Так же проще разрабатывать. Вы гарантируете, что подклассы не будут существовать, если объект неизменен. Например, Stringкласс.

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


Я не понимаю ни части об отправке по сети, ни той части, в которой вы говорите, что неизменяемый класс не может быть расширен.
aioobe,

@aioobe, отправка данных по сети, например, веб-сервисов, RMI и т. д. А для расширенного вы не можете расширять неизменяемый класс String, или
ImmutableSet, ImmutableList

Невозможность расширить String, ImmutableSet, ImmutableList и т. Д. Является полностью ортогональной проблемой неизменяемости. Не все классы, отмеченные finalв Java, являются неизменяемыми, и не все неизменяемые классы отмечены final.
ТОЛЬКО МОЕ ПРАВИЛЬНОЕ МНЕНИЕ

5

Я собираюсь атаковать это с другой точки зрения. Я считаю, что неизменяемые объекты облегчают мне жизнь при чтении кода.

Если у меня есть изменяемый объект, я никогда не уверен, каково его значение, если он когда-либо использовался за пределами моей непосредственной области действия. Скажем, я создаю MyMutableObjectлокальные переменные метода, заполняю их значениями, а затем передаю их пяти другим методам. ЛЮБОЙ из этих методов может изменить состояние моего объекта, поэтому должно произойти одно из двух:

  1. Я должен отслеживать тела пяти дополнительных методов, размышляя о логике своего кода.
  2. Мне нужно сделать пять расточительных защитных копий своего объекта, чтобы гарантировать, что правильные значения будут переданы каждому методу.

Первое затрудняет рассуждение о моем коде. Второе снижает производительность моего кода - я в основном имитирую неизменяемый объект с семантикой копирования при записи, но делаю это постоянно, независимо от того, действительно ли вызываемые методы изменяют состояние моего объекта.

Если я вместо этого использую MyImmutableObject, я могу быть уверен, что значения, которые я установил, будут такими, какими они будут в течение всего срока службы моего метода. Нет никакого «жуткого действия на расстоянии», которое изменит его из-под меня, и мне не нужно делать защитные копии моего объекта перед вызовом пяти других методов. Если другие методы хотят изменить что-то для своих целей, они должны сделать копию, но они делают это только в том случае, если им действительно нужно сделать копию (в отличие от того, что я делал перед каждым вызовом внешнего метода). Я избавляю себя от мысленных ресурсов отслеживания методов, которых может даже не быть в моем текущем исходном файле, и избавляю систему от накладных расходов на бесконечное создание ненужных защитных копий на всякий случай.

(Если я выйду за пределы мира Java и, скажем, в мир C ++, среди прочего, я могу стать еще сложнее. Я могу сделать объекты изменяемыми, но за кулисами сделать их прозрачным клонированием на любом своего рода изменение состояния - это копирование при записи - никто не станет мудрее.)


Одним из преимуществ изменяемых типов значений на языках, которые их поддерживают и не допускают постоянных ссылок на них, является то, что вы получаете описываемые вами преимущества (если изменяемый ссылочный тип передается по ссылке в подпрограмму, эта процедура может изменять его во время выполнения. , но не может вызывать модификации после возврата) без некоторой неуклюжести, которую часто имеют неизменяемые объекты.
supercat

5

Мои 2 цента будущим посетителям:


2 сценария, в которых неизменяемые объекты являются хорошим выбором:

В многопоточности

Проблемы параллелизма в многопоточной среде могут быть решены с помощью синхронизации, но синхронизация является дорогостоящим делом (не буду копаться здесь в «почему»), поэтому, если вы используете неизменяемые объекты, тогда нет синхронизации для решения проблемы параллелизма из-за состояния неизменяемые объекты не могут быть изменены, и если состояние не может быть изменено, все потоки могут беспрепятственно получить доступ к объекту. Таким образом, неизменяемые объекты - отличный выбор для общих объектов в многопоточной среде.


В качестве ключа для коллекций на основе хешей

Одна из наиболее важных вещей, на которые следует обратить внимание при работе с коллекцией на основе хэшей, заключается в том, что ключ должен быть таким, hashCode()чтобы он всегда возвращал одно и то же значение в течение всего времени жизни объекта, потому что если это значение изменяется, тогда старая запись сделана в коллекции на основе хеша использование этого объекта не может быть получено, следовательно, это может вызвать утечку памяти. Поскольку состояние неизменяемых объектов не может быть изменено, они являются отличным выбором в качестве ключа в коллекции на основе хэшей. Итак, если вы используете неизменяемый объект в качестве ключа для коллекции на основе хэша, вы можете быть уверены, что из-за этого не будет утечки памяти (конечно, все еще может быть утечка памяти, когда объект, используемый как ключ, не упоминается из любого места иначе, но дело не в этом).


2

Неизменяемые объекты - это экземпляры, состояния которых не меняются после инициации. Использование таких объектов зависит от требований.

Неизменяемый класс хорош для целей кеширования и является потокобезопасным.


2

Благодаря неизменности вы можете быть уверены, что поведение / состояние базового неизменяемого объекта не изменится, при этом вы получите дополнительное преимущество выполнения дополнительных операций:

  • Вы можете легко использовать несколько ядер / процессоров ( параллельная / параллельная обработка ) (поскольку последовательность операций больше не имеет значения).

  • Может делать кеширование для дорогостоящих операций (так как вы уверены в том же
    результате).

  • Может легко выполнять отладку (так как история выполнения больше не будет проблемой
    )


1

Использование ключевого слова final не обязательно делает что-то неизменным:

public class Scratchpad {
    public static void main(String[] args) throws Exception {
        SomeData sd = new SomeData("foo");
        System.out.println(sd.data); //prints "foo"
        voodoo(sd, "data", "bar");
        System.out.println(sd.data); //prints "bar"
    }

    private static void voodoo(Object obj, String fieldName, Object value) throws Exception {
        Field f = SomeData.class.getDeclaredField("data");
        f.setAccessible(true);
        Field modifiers = Field.class.getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(f, f.getModifiers() & ~Modifier.FINAL);
        f.set(obj, "bar");
    }
}

class SomeData {
    final String data;
    SomeData(String data) {
        this.data = data;
    }
}

Это просто пример, демонстрирующий, что ключевое слово final используется для предотвращения ошибки программиста, и не более того. В то время как переназначение значения без ключевого слова final может легко произойти случайно, переход на эту длину для изменения значения должен быть сделан намеренно. Он предназначен для документации и предотвращения ошибок программиста.


Обратите внимание, что это не дает прямого ответа на вопрос, поскольку речь идет не об окончании; речь шла о неизменяемых классах. У вас может быть неизменный класс без ключевого слова final, используя соответствующие ограничения доступа.
Марк Питерс,

1

Неизменяемые структуры данных также могут помочь при кодировании рекурсивных алгоритмов. Например, предположим, что вы пытаетесь решить задачу 3SAT . Один из способов - сделать следующее:

  • Выберите неназначенную переменную.
  • Присвойте ему значение ИСТИНА. Упростите экземпляр, удалив предложения, которые теперь удовлетворены, и повторите их для решения более простого экземпляра.
  • Если рекурсия в случае ИСТИНА не удалась, то вместо этого присвойте этой переменной ЛОЖЬ. Упростите этот новый экземпляр и повторите его, чтобы решить.

Если у вас есть изменяемая структура для представления проблемы, то при упрощении экземпляра в ветви TRUE вам либо придется:

  • Следите за всеми изменениями, которые вы вносите, и отмените их все, как только поймете, что проблему невозможно решить. Это имеет большие накладные расходы, потому что ваша рекурсия может быть довольно глубокой, и ее сложно кодировать.
  • Сделайте копию экземпляра, а затем измените копию. Это будет медленным, потому что, если ваша рекурсия имеет глубину в несколько десятков уровней, вам придется сделать много копий экземпляра.

Однако, если вы умно закодируете его, у вас может быть неизменяемая структура, где любая операция возвращает обновленную (но все еще неизменяемую) версию проблемы (аналогично String.replace- она ​​не заменяет строку, а просто дает вам новую ). Наивный способ реализовать это - сделать так, чтобы «неизменяемая» структура просто копировала и создавала новую при любой модификации, сводя ее ко второму решению при наличии изменяемого, со всеми этими накладными расходами, но вы можете сделать это более эффективный способ.


1

Одна из причин «потребности» в неизменяемых классах - комбинация передачи всего по ссылке и отсутствия поддержки представлений объекта только для чтения (то есть C ++ const).

Рассмотрим простой случай класса, поддерживающего шаблон наблюдателя:

class Person {
    public string getName() { ... }
    public void registerForNameChange(NameChangedObserver o) { ... }
}

Если бы stringони не были неизменяемыми, было бы невозможно правильно Personреализовать класс registerForNameChange(), потому что кто-то мог бы написать следующее, эффективно изменяя имя человека, не вызывая никаких уведомлений.

void foo(Person p) {
    p.getName().prepend("Mr. ");
}

В C ++ getName()возврат a const std::string&имеет эффект возврата по ссылке и предотвращения доступа к мутаторам, что означает, что неизменяемые классы в этом контексте не нужны.



1

Одна особенность неизменяемых классов, которая еще не была вызвана: хранение ссылки на глубоко неизменяемый объект класса является эффективным средством хранения всего содержащегося в нем состояния. Предположим, у меня есть изменяемый объект, который использует глубоко неизменяемый объект для хранения 50 КБ информации о состоянии. Предположим, далее, что я хочу 25 раз сделать «копию» моего исходного (изменяемого) объекта (например, для буфера «отмены»); состояние может меняться между операциями копирования, но обычно этого не происходит. Создание «копии» изменяемого объекта просто потребует копирования ссылки на его неизменяемое состояние, поэтому 20 копий будут просто равны 20 ссылкам. Напротив, если бы состояние содержалось в изменяемых объектах стоимостью 50К, каждая из 25 операций копирования должна была бы создавать свою собственную копию данных стоимостью 50К; хранение всех 25 копий потребовало бы хранения более одного мегабайта данных, в основном дублированных. Несмотря на то, что первая операция копирования создаст копию данных, которая никогда не изменится, а остальные 24 операции теоретически могут просто ссылаться на нее, в большинстве реализаций не было бы возможности для второго объекта, запрашивающего копию информация, чтобы знать, что неизменная копия уже существует (*).

(*) Один из шаблонов, который иногда может быть полезен, заключается в том, что изменяемые объекты имеют два поля для хранения своего состояния - одно в изменяемой форме и одно в неизменяемой форме. Объекты можно копировать как изменяемые или неизменяемые, и они начнут свою жизнь с того или иного набора ссылок. Как только объект хочет изменить свое состояние, он копирует неизменяемую ссылку на изменяемую (если это еще не было сделано) и аннулирует неизменяемую. Когда объект копируется как неизменяемый, если его неизменяемая ссылка не установлена, будет создана неизменная копия, и неизменная ссылка укажет на нее. Этот подход потребует несколько дополнительных операций копирования, чем "полноценная копия при записи" (например, запрос на копирование объекта, который был изменен с момента последней копии, потребует операции копирования,


1

Почему неизменяемый класс?

После создания экземпляра объекта его состояние не может быть изменено за время жизни. Что также делает его потокобезопасным.

Примеры :

Очевидно, String, Integer и BigDecimal и т. Д. После того, как эти значения созданы, их нельзя изменить в течение жизни.

Вариант использования: после создания объекта подключения к базе данных с его значениями конфигурации вам может не потребоваться изменять его состояние, где вы можете использовать неизменяемый класс.


0

из эффективной Java; Неизменяемый класс - это просто класс, экземпляры которого нельзя изменить. Вся информация, содержащаяся в каждом экземпляре, предоставляется при его создании и фиксируется на время существования объекта. Библиотеки платформы Java содержат множество неизменяемых классов, в том числе String, упакованные примитивные классы, а также BigInteger и BigDecimal. Для этого есть много веских причин: неизменяемые классы легче разрабатывать, реализовывать и использовать, чем изменяемые классы. Они менее подвержены ошибкам и более безопасны.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.