Навіщо нам незмінний клас?


83

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


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

Відповіді:


82

Інші відповіді здаються надто зосередженими на поясненні, чому незмінність хороша. Це дуже добре, і я використовую його, коли це можливо. Однак це не ваше питання . Я буду брати ваше запитання по пунктах, щоб спробувати переконатися, що ви отримуєте відповіді та приклади, які вам потрібні.

Я не можу зрозуміти, за якими сценаріями нам потрібен незмінний клас.

"Потреба" тут є відносним терміном. Незмінні класи - це шаблон дизайну, який, як і будь-яка парадигма / шаблон / інструмент, є для спрощення побудови програмного забезпечення. Так само було написано багато коду до появи парадигми ОО, але зарахуйте мене до числа програмістів, які "потребують" ОО. Незмінні класи, такі як ОО, суворо не потрібні , але я буду діяти так, як вони мені потрібні.

Ви коли-небудь стикалися з якоюсь такою вимогою?

Якщо ви не дивитесь на об'єкти в проблемному домені з правильною перспективою, ви можете не побачити вимоги до незмінного об'єкта. Може бути легко думати, що проблемний домен не вимагає незмінних класів, якщо ви не знайомі з тим, коли їх вигідно використовувати.

Я часто використовую незмінні класи, коли я вважаю даний об'єкт у моєму проблемному домені значенням або фіксованим екземпляром . Це поняття іноді залежить від перспективи чи точки зору, але в ідеалі легко буде перейти у правильну перспективу, щоб визначити добрі об'єкти-кандидати.

Ви можете краще зрозуміти, де незмінні об’єкти дійсно корисні (якщо не суворо необхідні), переконавшись, що читаєте різні книги / Інтернет-статті, щоб сформувати хороший сенс, як думати про незмінні класи. Одна хороша стаття для початку - це теорія та практика Java: мутувати чи не мутувати?

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

... чи можете ви дати нам будь-який реальний приклад, коли ми повинні використовувати цей шаблон?

Оскільки ви запитували реальні приклади, я дам вам кілька, але спочатку почнемо з деяких класичних прикладів.

Класичні об'єкти вартості

Рядки та цілі числа часто розглядаються як значення. Тому не дивно, що клас String і клас обгортки Integer (як і інші класи обгортки) є незмінними в Java. Зазвичай колір розглядається як значення, отже, незмінний клас Color.

Контрприклад

На відміну від цього, про автомобіль зазвичай не думають як про об’єкт цінності. Моделювання автомобіля, як правило, означає створення класу, який має мінливий стан (одометр, швидкість, рівень палива тощо). Однак є деякі домени, де це автомобіль може бути об'єктом цінності. Наприклад, автомобіль (або конкретно модель автомобіля) можна розглядати як ціннісний об’єкт у програмі для пошуку належного моторного масла для даного автомобіля.

Гральні карти

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

Однак я, як правило, вважаю ігрову карту незмінним об'єктом, який має незмінну масть і ранг, коли-небудь створений. Моя розіграш у розіграші покеру складатиме 5 екземплярів, а заміна карти у моїй руці передбачала б відкидання одного з цих примірників та додавання нового випадкового примірника до моєї руки.

Проекція карт

Останній приклад - це коли я працював над кодом карти, де карта могла відображати себе в різних проекціях . В оригінальному коді карта використовувала фіксований, але мінливий екземпляр проекції (як змінна гральна карта вище). Зміна проекції карти означала мутацію іварів екземпляра проекції карти (тип проекції, центральна точка, масштабування тощо).

Однак я вважав, що дизайн був простішим, якщо я думав про проекцію як про незмінне значення або фіксовану інстанцію. Зміна проекції карти означало наявність посилання на карту в іншому екземплярі проекції, а не мутацію фіксованого екземпляра проекції карти. Це також спростило захоплення іменованих проекцій, таких як MERCATOR_WORLD_VIEW.


42

Незмінні класи загалом набагато простіші у розробці, реалізації та правильному використанні . Прикладом є String: реалізація java.lang.Stringзначно простіша, ніж std::stringу C ++, здебільшого через її незмінність.

Однією з особливостей незмінності особливо великої різниці є паралельність: незмінні об’єкти можуть безпечно ділитися між кількома потоками , тоді як змінні об’єкти повинні бути захищені від потоків за допомогою ретельного проектування та реалізації - зазвичай це далеко не тривіальне завдання.

Оновлення: Ефективне друге видання Java вирішує цю проблему детально - див. Пункт 15: Мінімізуйте зміни .

Дивіться також ці пов’язані дописи:


39

Ефективна Java від Джошуа Блоха окреслює кілька причин писати незмінні класи:

  • Простота - кожен клас знаходиться лише в одному стані
  • Thread Safe - оскільки стан не можна змінити, синхронізація не потрібна
  • Написання у незмінному стилі може призвести до отримання більш надійного коду. Уявіть, якби рядки не були незмінними; Будь-які методи отримання, які повернули рядок, вимагатимуть реалізації для створення захисної копії перед поверненням рядка - інакше клієнт може випадково або зловмисно порушити цей стан об’єкта.

Як правило, є доброю практикою робити об’єкт незмінним, якщо в результаті не виникають серйозні проблеми з продуктивністю. За таких обставин змінні об’єкти будівельника можуть бути використані для створення незмінних об’єктів, наприклад StringBuilder


1
Кожен клас знаходиться в одному стані або в кожному об’єкті?
Тушар Тхакур

@TusharThakur, кожен об'єкт.
жартівник

17

Класичним прикладом є хеш-карти. Обов’язково, щоб ключ до карти був незмінним. Якщо ключ не є незмінним, і ви змінили значення на ключі таким чином, що hashCode () призведе до нового значення, карта тепер порушена (ключ зараз знаходиться в неправильному місці в хеш-таблиці.).


3
Я б скоріше сказав, що вкрай важливо не міняти ключ; офіційних вимог щодо його незмінності немає.
Péter Török

Не впевнений, що ви маєте на увазі під "офіційним".
Kirk Woll,

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були змінними. О, почекайте, вони є, ОН SH
gustafc

1
@gustafc: Змінювати календар - це нормально, враховуючи його роботу - обчислювати дату (це можна зробити, повернувши копії, але враховуючи, наскільки важким є Календар, краще таким чином). але Дата - так, це неприємно.
Michael Borgwardt

На деяких реалізаціях JRE Stringможна змінювати! (підказка: деякі старіші версії JRockit). Виклик string.trim () призвів до того, що оригінальний рядок був обрізаний
Salandur

6
@Salandur: Тоді це не "реалізація JRE". Це реалізація чогось, що нагадує JRE, але не є.
Mark Peters

@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"; Якщо я не змінній не стану кінцевим, я не можу бути впевненим що це не змінюється в моєму власному блоці коду. Так само, як цілі константи запевняють мене, що "1" - це завжди одне і те ж число, але не те, що "x = 1" ніколи не буде змінено, написавши "x = 2". Але я може бути впевненим, що якщо у мене є дескриптор незмінного об’єкта, жодна функція, якій я передаю його, не може змінити його на мене, або що якщо я зроблю дві копії, що зміна змінної, що містить одну копію, не змінить інше тощо.


5

Існують різні причини незмінності:

  • Безпека ниток: незмінні об’єкти не можна змінити, а також не може змінити їх внутрішній стан, тому немає необхідності синхронізувати його.
  • Це також гарантує, що все, що я надсилаю через (через мережу), має надходити в тому ж стані, що й раніше. Це означає, що ніхто (підслуховувач) не може прийти і додати випадкові дані до мого незмінного набору.
  • Це також простіше у розробці. Ви гарантуєте, що ніяких підкласів не буде, якщо об’єкт незмінний. Наприклад, Stringклас.

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


Я не отримую ні частини про передачу через мережу, ні тієї частини, в якій ви говорите, що незмінний клас не можна розширити.
aioobe

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

Неможливість розширити String, ImmutableSet, ImmutableList тощо є абсолютно ортогональною проблемою незмінності. Не всі класи, позначені finalв Java, незмінні, і не всі незмінні класи позначені final.
ТІЛЬКИ МОЙ правильний ДУМКА

5

Я збираюся напасти на це з іншої точки зору. Я вважаю, що незмінні предмети полегшують мені життя під час читання коду.

Якщо у мене є змінний об'єкт, я ніколи не знаю, яка його цінність, якщо він коли-небудь використовується поза моїм безпосереднім обсягом. Скажімо, я створюю MyMutableObjectв локальних змінних методу, заповнюю його значеннями, а потім передаю п’яти іншим методам. БУДЬ-ЯКИЙ із цих методів може змінити стан мого об’єкта, тому має відбутися одна з двох речей:

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

Перше ускладнює міркування щодо мого коду. Друге змушує мій код забивати продуктивність - я в основному імітую незмінний об'єкт за допомогою семантики copy-on-write, але роблю це постійно, незалежно від того, чи викликані методи насправді змінюють стан мого об'єкта.

Якщо я замість цього застосую MyImmutableObject, я можу бути впевнений, що те, що я встановив, буде таким, яким буде значення для життя мого методу. Немає "моторошної дії на відстані", яка змінила б її з-під мене, і мені не потрібно робити захисні копії мого об'єкта, перш ніж використовувати п'ять інших методів. Якщо інші методи хочуть змінити ситуацію відповідно до своїх цілей, вони повинні зробити копію - але вони роблять це лише в тому випадку, якщо їм дійсно потрібно зробити копію (на відміну від того, що я робив це перед кожним викликом зовнішнього методу). Я шкодую собі розумові ресурси для відстеження методів, яких навіть немає в моєму поточному вихідному файлі, і шкодую системі накладних витрат на нескінченне створення непотрібних захисних копій про всяк випадок.

(Якщо я вийду за межі світу Java і, скажімо, в світ С ++, серед інших, я можу стати ще складнішим. Я можу зробити так, щоб об'єкти здавалися немов змінними, але за лаштунками зробити їх прозорим клонуванням на будь-якому така зміна стану - це копіювання на запис - при цьому ніхто не мудріший.)


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

5

Мої 2 центи для майбутніх відвідувачів:


2 сценарії, коли незмінні об’єкти є правильним вибором:

У багатопоточності

Проблеми з паралельністю в багатопотоковому середовищі цілком можна вирішити за допомогою синхронізації, але синхронізація є дорогою справою (не варто тут копати "чому"), тому, якщо ви використовуєте незмінні об'єкти, тоді немає синхронізації для вирішення проблеми паралельності через стан незмінні об'єкти не можна змінити, а якщо стан неможливо змінити, тоді всі потоки можуть безперешкодно отримувати доступ до об'єкта. Отже, незмінні об’єкти роблять чудовий вибір для спільних об’єктів у багатопотоковому середовищі.


Як ключ для колекцій на основі хешу

Одне з найважливіших моментів, на які слід звернути увагу при роботі з колекцією на основі хешу, це те, що ключ повинен бути таким, щоб він hashCode()завжди повертав одне і те ж значення протягом усього життя об’єкта, тому що якщо це значення змінено, то старий запис, зроблений у колекції на основі хешу використання цього об'єкта не може бути отримано, отже, це призведе до витоку пам'яті. Оскільки стан незмінних об'єктів не можна змінити, тому вони роблять чудовий вибір як ключ у колекції на основі хешу. Отже, якщо ви використовуєте незмінний об'єкт як ключ для збору на основі хешу, ви можете бути впевнені, що через це не буде витоку пам'яті (звичайно, все одно може бути витік пам'яті, коли на об'єкт, що використовується як ключ, ніде не вказано посилання в іншому, але справа не в цьому).


2

Незмінні об'єкти - це екземпляри, стани яких не змінюються після ініціювання. Використання таких об'єктів залежить від вимог.

Незмінний клас добре підходить для кешування та безпечний для потоків.


2

Завдяки незмінності ви можете бути впевнені, що поведінка / стан основного незмінного об'єкта не зміниться, завдяки чому ви отримаєте додаткову перевагу при виконанні додаткових операцій:

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

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

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


1

Використання кінцевого ключового слова не обов'язково робить щось незмінним:

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" існує для запобігання помилкам програміста, і не набагато більше. У той час як перепризначення значення, у якому відсутнє кінцеве ключове слово, може легко трапитися випадково, перехід до такої довжини для зміни значення повинен був здійснюватися навмисно. Тут є документація та запобігання помилкам програміста.


Зауважте, що це прямо не відповідає на питання, оскільки мова не йшла про остаточний; йшлося про незмінні класи. Ви можете мати незмінний клас без остаточного ключового слова, використовуючи належні обмеження доступу.
Mark Peters

1

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

  • Виберіть непризначену змінну.
  • Дайте йому значення TRUE. Спростіть екземпляр, витягнувши відповідні умови, і повторіть, щоб вирішити простіший екземпляр.
  • Якщо рекурсія у випадку TRUE не вдалася, тоді присвоїти цю змінну FALSE. Спростіть цей новий екземпляр і повторіть його вирішення.

Якщо у вас є змінна структура, яка представляє проблему, тоді, коли ви спростите екземпляр у гілці TRUE, вам доведеться:

  • Відстежуйте всі внесені вами зміни та скасуйте їх, як тільки зрозумієте, що проблему неможливо вирішити. Це має великі накладні витрати, тому що ваша рекурсія може піти досить глибоко, і це складно кодувати.
  • Зробіть копію екземпляра, а потім змініть копію. Це буде повільно, тому що якщо ваша рекурсія глибиною кілька десятків рівнів, вам доведеться зробити багато копій екземпляра.

Однак якщо ви розумно кодуєте його, ви можете мати незмінну структуру, де будь-яка операція повертає оновлену (але все ще незмінну) версію проблеми (подібно до String.replace- вона не замінює рядок, а просто дає вам нову ). Наївний спосіб реалізувати це полягає в тому, щоб "незмінна" структура просто копіювала і створювала нову при будь-якій модифікації, зменшуючи її до 2-го рішення при наявності змінного, з усіма накладними витратами, але ви можете зробити це в більш ефективний спосіб.


1

Однією з причин "необхідності" незмінних класів є комбінація передачі всього за посиланням і відсутність підтримки переглядів об'єкта (лише для читання 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.