Чому Java 8 не включає незмінні колекції?


130

Команда Java здійснила велику роботу, усуваючи бар'єри для функціонального програмування на Java 8. Зокрема, зміни в колекціях java.util виконують велику роботу з ланцюжка перетворень на дуже швидкі потокові операції. Зважаючи на те, наскільки добре вони виконали додавання першокласних функцій та функціональних методів у колекції, чому вони повністю не змогли надати незмінних колекцій чи навіть незмінних інтерфейсів колекції?

Не змінюючи жодного існуючого коду, команда Java могла б у будь-який час додати незмінних інтерфейсів, таких самих, як і ті, що змінюються, за вирахуванням методів "встановити" та змусити існуючі інтерфейси поширюватися з них, як це:

                  ImmutableIterable
     ____________/       |
    /                    |
Iterable        ImmutableCollection
   |    _______/    /          \   \___________
   |   /           /            \              \
 Collection  ImmutableList  ImmutableSet  ImmutableMap  ...
    \  \  \_________|______________|__________   |
     \  \___________|____________  |          \  |
      \___________  |            \ |           \ |
                  List            Set           Map ...

Зрозуміло, такі операції, як List.add () та Map.put (), в даний час повертають булеве або попереднє значення для даного ключа, щоб вказати, чи вдалася операція чи не вдалася. Незмінні колекції повинні були б розглядати такі методи як фабрики та повертати нову колекцію, що містить доданий елемент - що несумісне з поточним підписом. Але це можна вирішити, використовуючи іншу назву методу, як ImmutableList.append () або .addAt () та ImmutableMap.putEntry (). Отримане багатослів’я було б більш ніж переважене на користь роботи з незмінними колекціями, а система типів запобігла б помилкам викликати неправильний метод. З часом старі методи можуть бути застарілими.

Виграші незмінних колекцій:

  • Простота - міркування про код простіше, коли основні дані не змінюються.
  • Документація - якщо метод приймає незмінний інтерфейс колекції, ви знаєте, що він не збирається змінювати цю колекцію. Якщо метод повертає незмінну колекцію, ви знаєте, що ви не можете її змінити.
  • Паралельність - незмінні колекції можна безпечно ділити по нитках.

Як хтось, хто скуштував мови, які передбачають незмінність, повернутися на Дикий Захід бурхливих мутацій дуже важко. У колекціях Clojure (абстракція послідовностей) вже є все, що надають колекції Java 8, плюс незмінність (хоча можливо використання додаткової пам’яті та часу завдяки синхронізованим спискам зв’язків замість потоків). Scala має як змінні, так і незмінні колекції з повним набором операцій, і хоча ці операції прагнуть, виклик .iterator надає лінивий погляд (і є інші способи ліниво оцінити їх). Я не бачу, як Java може продовжувати конкурувати без незмінних колекцій.

Чи може хтось вказати мені на історію чи дискусію з цього приводу? Звичайно, це десь публічно.


9
Пов’язане з цим - Ayende нещодавно блогував про колекції та незмінні колекції в C #, з орієнтирами. ayende.com/blog/tags/performance - tl; dr - незмінність повільна .
Одід

20
з вашою ієрархією я можу дати вам ImmutableList, а потім змінити його на вас, коли ви не очікуєте, що це може зламати багато речей, як це у вас є лише constколекції
храповик виродка

18
@ Оценована незмінність повільна, але це блокування. Так само збереження історії. Простота / коректність вартує швидкості у багатьох ситуаціях. З невеликими колекціями швидкість - це не проблема. Аналіз Ayende заснований на припущенні, що вам не потрібна історія, блокування чи простота, і що ви працюєте з великим набором даних. Іноді це правда, але це не завжди є найкращою справою. Є компроміси.
GlenPeterson

5
@GlenPeterson - це те, що захисні копії і Collections.unmodifiable*()для чого. але не ставтесь до них як до незмінних, коли їх немає
грохот вирод

13
Так? Якщо ваша функція бере участь ImmutableListу цій діаграмі, люди можуть передавати змінні List? Ні, це дуже погане порушення LSP.
Теластин

Відповіді:


113

Тому що незмінні колекції абсолютно потребують спільного використання для використання. В іншому випадку кожна окрема операція кудись купляє цілий інший список. Мови, які повністю незмінні, як Haskell, генерують дивовижні кількості сміття без агресивних оптимізацій та обміну. Маючи колекцію, яка може бути використана лише з <50 елементами, не варто розміщувати в стандартній бібліотеці.

Більше того, незмінні колекції часто мають принципово іншу реалізацію, ніж їх колеги, що змінюються. Розглянемо, наприклад ArrayList, ефективний незмінний ArrayListзовсім не буде масивом! Його слід реалізувати за допомогою збалансованого дерева з великим коефіцієнтом розгалуження, Clojure використовує 32 IIRC. Зробити змінні колекції "непорушними", просто додавши функціональне оновлення - це помилка продуктивності настільки ж, як і витік пам'яті.

Крім того, обмін не є життєздатним на Java. Java надає занадто багато необмежених гачків для змінності та рівності еталонів, щоб зробити обмін "просто оптимізацією". Напевно, вас би трохи роздратувало, якби ви могли змінити елемент у списку, і зрозуміли, що ви просто змінили елемент у інших 20 версіях цього списку, які ви мали.

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


21
Мій приклад говорив про незмінні інтерфейси . Java могла б забезпечити повний набір як змінних, так і незмінних реалізацій тих інтерфейсів, які могли б зробити необхідні компроміси. Програміст вирішує вибрати мутабельний або незмінний у відповідних випадках. Програмісти повинні знати, коли використовувати список проти встановити зараз. Як правило, не потрібна змінна версія, поки у вас не виникне проблема з продуктивністю, і тоді вона може знадобитися лише як конструктор. У будь-якому випадку, мати непорушний інтерфейс - це виграш самостійно.
GlenPeterson

4
Я ще раз прочитав вашу відповідь, і, думаю, ви говорите, що Java має принципове припущення про змінність (наприклад, квасоля java) і що колекції - лише верхівка айсберга, і вирізання цього наконечника не вирішить основної проблеми. Дійсний пункт. Я міг би прийняти цю відповідь і прискорити своє прийняття Scala! :-)
GlenPeterson

8
Я не впевнений, що незмінні колекції вимагають вміння ділитися загальними частинами, щоб бути корисними. Найпоширеніший незмінний тип на Java - це незмінна колекція символів, яка використовується для надання доступу до спільного доступу, але більше не працює. Ключова річ, яка робить її корисною - це можливість швидко копіювати дані з а Stringв StringBuffer, маніпулювати ними, а потім копіювати дані в нові незмінні String. Використання такого шаблону з наборами та списками може бути таким же корисним, як використання непорушних типів, розроблених для полегшення створення трохи змінених екземплярів, але все-таки можуть бути кращими ...
supercat

3
Цілком можливо зробити незмінну колекцію в Java за допомогою спільного використання. Елементи, що зберігаються в колекції, є посиланнями, і їх референти можуть бути вимкнено - ну і що? Така поведінка вже порушує існуючі колекції, такі як HashMap і TreeSet, але ті реалізовані в Java. І якщо кілька колекцій містять посилання на один і той же об’єкт, цілком очікується, що зміна об'єкта призведе до зміни, видимої при перегляді з усіх колекцій.
Секрет Соломонофа

4
jozefg, цілком можливо реалізувати ефективні незмінні колекції на JVM зі структурним обміном. Scala і Clojure мають їх як частину своєї стандартної бібліотеки, обидві реалізації базуються на HAMT Філа Багвелла (Hash Array Mapped Trie). Ваша заява щодо Clojure, що реалізує незмінні структури даних з BALANCED деревами, абсолютно помилкова.
sesm

77

Колекція, що змінюється, не є підтипом незмінної колекції. Натомість колекції, що змінюються та змінюються, є нащадками читаних колекцій. На жаль, поняття "читабельні", "лише для читання" та "незмінні", здається, розмиваються разом, хоча вони означають три різні речі.

  • Читаючий базовий клас колекції або тип інтерфейсу обіцяє, що можна читати елементи та не надає жодних прямих засобів зміни колекції, але не гарантує, що код, що отримує посилання, не може передавати або маніпулювати ним таким чином, щоб дозволити зміни.

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

  • Незмінна колекція - це та, яка завжди буде зберігати однакові дані, поки існують будь-які посилання на неї. Будь-яка реалізація незмінного інтерфейсу, який не завжди повертає однакові дані у відповідь на конкретний запит, порушується.

Іноді корисно мати сильно пов'язані змінювані і незмінні типи колекцій , які й реалізують або випливають з того ж «читаного» типу і мають читаються типу включають в себе AsImmutable, AsMutableі AsNewMutableметоду. Така конструкція може дозволити виклик коду, який хоче зберігати дані в колекції AsImmutable; цей метод зробить захисну копію, якщо колекція є змінною, але пропустіть її, якщо вона вже непорушна.


1
Чудова відповідь. Незмінні колекції можуть дати вам досить міцну гарантію, пов’язану з безпекою ниток і тим, як ви можете міркувати про них з часом. Колекція для читання / лише для читання не робить. Насправді, щоб дотримуватися принципу заміщення Ліскова, "Лише для читання" та "Незмінне", ймовірно, повинен бути абстрактний базовий тип із кінцевим методом та приватними членами, щоб гарантувати, що жоден похідний клас не може зруйнувати гарантію, надану типом. Або вони повинні бути повністю конкретного типу, які або обгортають колекцію (лише для читання), або завжди беруть захисну копію (Immutable). Ось як це робить guava ImmutableList.
Лоран Буро-Рой

1
@ LaurentBourgault-Roy: Є переваги як у герметичних, так і у спадок незмінних типів. Якщо не хочеться дозволити нелегітимному похідному класу зламати інваріанти, запечатані типи можуть забезпечити захист від цього, тоді як спадкові класи не пропонують жодного. З іншого боку, код, який щось знає про дані, які він містить, може зберігати його набагато компактніше, ніж тип, який нічого про нього не знає. Розглянемо, наприклад, тип ReadableIndexedIntSequence, який інкапсулює послідовність intметодів getLength()та getItemAt(int).
supercat

1
@ LaurentBourgault-Roy: З огляду на a ReadableIndexedIntSequence, можна створити екземпляр непорушного типу масиву, скопіювавши всі елементи в масив, але припустимо, що конкретна реалізація просто повертала 16777216 за довжиною та ((long)index*index)>>24для кожного елемента. Це була б законна незмінна послідовність цілих чисел, але їх копіювання в масив було б величезною тратою часу та пам'яті.
supercat

1
Я повністю згоден. Моє рішення дає вам правильність (до певного моменту), але для отримання продуктивності з великим набором даних, ви повинні мати стійку структуру та дизайн для незмінності з самого початку. Для невеликої колекції, хоча час від часу ви можете піти, беручи непомітну копію. Я пам’ятаю, що Скала робив аналіз різних програм і виявив, що щось на зразок 90% створених списків містило 10 і менше предметів.
Лоран Буро-Рой

1
@ LaurentBourgault-Roy: Основне питання полягає в тому, чи можна довіряти людям не виробляти розбиті реалізації чи похідні класи. Якщо це так, і якщо інтерфейси / базові класи надають методи asMutable / asImmutable, можливо, можна підвищити продуктивність на багато порядків [наприклад, порівняти вартість виклику asImmutableекземпляра визначеної вище послідовності порівняно з витратами на побудову незмінна копія масиву]. Я б сказав, що мати інтерфейси, визначені для таких цілей, можливо, краще, ніж намагатися використовувати спеціальні підходи; ІМХО, найбільша причина ...
supercat

15

Java Collections Framework надає можливість створювати колекцію лише для читання, використовуючи шість статичних методів у класі java.util.Collections :

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

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


4
Дизайн немодифікованих колекцій був би значно вдосконалений, якби на обгортках містилося вказівка ​​того, чи обгорнута річ вже непорушна, і існують і immutableListт. Д. Заводські методи, які б повернули обгортку, котру можна прочитати, навколо копії переданого файлу список, якщо список, що передається, був уже незмінним . Створити такі визначені користувачем типи було б легко, але для однієї проблеми: joesCollections.immutableListметод не визнав би, що він не повинен копіювати повернений об'єкт fredsCollections.immutableList.
supercat

8

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

Процент програмістів java знає про існування unmodifiableCollection()родини методів Collectionsкласу, але навіть серед них багато хто просто не турбуються.

І я не можу їх звинувачувати: інтерфейс, який прикидається читанням-записом, але викине, UnsupportedOperationExceptionякщо ви помилитесь, використовуючи будь-який із його методів «запису», - це зовсім зле!

Тепер інтерфейс, як у Collectionякому не було б add(), remove()а clear()методи не будуть інтерфейсом "ImmutableCollection"; це буде інтерфейс "UnmodifiableCollection". Насправді, ніколи не може бути інтерфейсу "ImmutableCollection", оскільки незмінність - це характер реалізації, а не характеристика інтерфейсу. Я знаю, це не дуже зрозуміло; дозволь пояснити.

Припустимо, хтось передає вам такий інтерфейс колекції, доступний лише для читання; чи безпечно передати його в іншу нитку? Якби ви точно знали, що це справді непорушна колекція, то відповідь буде «так»; на жаль, оскільки це інтерфейс, ви не знаєте, як він реалізований, тому відповідь має бути " ні" : оскільки всі ви знаєте, це може бути немодифікований (для вас) перегляд колекції, яка насправді є зміною, (як, наприклад, те, що ви отримуєте з Collections.unmodifiableCollection()), тому спроба прочитати з нього, коли інший потік модифікує, призведе до читання пошкоджених даних.

Отже, те, що ви описали по суті, - це набір інтерфейсів колекції не "Immutable", а "Unmodifiable". Важливо розуміти, що "Немодифікується" просто означає, що той, хто має посилання на такий інтерфейс, не може змінювати базову колекцію, і їм заважають просто тому, що в інтерфейсі відсутні будь-які способи модифікації, а не тому, що базовий збірник обов'язково є незмінним. Основна колекція може бути цілком змінна; у вас немає знань і контролю над цим.

Для того щоб мати незмінні колекції, вони мали б бути класами , а не інтерфейсами!

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

Отже, для того, щоб мати повний набір колекцій у java (або будь-якій іншій декларативній імперативній мові), нам знадобиться наступне:

  1. Набір незмінних інтерфейсів колекції .

  2. Набір змінних інтерфейсів колекції , що розширюють незмінні.

  3. Набір класів колекцій, що змінюються, що реалізують інтерфейси, що змінюються, а також розширення також незмінні інтерфейси.

  4. Набір незмінних колекційних класів , що реалізують незмінні інтерфейси, але в основному передаються як класи, щоб гарантувати незмінність.

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

Причина, чому вони не є частиною виконання Java, мабуть, тому, що вважалося, що це буде занадто багато / занадто складно / занадто важко зрозуміти.

Особисто я вважаю, що те, що я описав вище, навіть недостатньо; Ще одна річ, яка, здається, потрібна - це набір змінних інтерфейсів і класів для структурної незмінністі . (Що може бути просто названо "Жорстким", тому що префікс "StrukturlyImmutable" занадто проклятий.)


Хороші бали. Дві деталі: 1. Колекції, що змінюються, вимагають певних підписів методів, зокрема (використовуючи список як приклад): List<T> add(T t)- всі "мутаторні" методи повинні повернути нову колекцію, що відображає зміни. 2. На краще чи гірше, Інтерфейси часто представляють контракт на додаток до підпису. Serializable є одним з таких інтерфейсів. Точно так же, Порівнянні вимагає , щоб ви правильно реалізувати compareTo()метод , щоб правильно працювати і в ідеалі бути сумісні з equals()і hashCode().
GlenPeterson

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

Чи доступна ваша реалізація? Я повинен був це запитати місяці тому. У будь-якому разі, моє: github.com/GlenKPeterson/UncleJim
GlenPeterson

4
Suppose someone hands you such a read-only collection interface; is it safe to pass it to another thread?Припустимо, хтось передає вам екземпляр інтерфейсу колекції, що змінюється. Чи безпечно викликати будь-який метод на ньому? Ви не знаєте, що реалізація не циркулює назавжди, викидає виняток або повністю ігнорує контракт інтерфейсу. Чому є подвійний стандарт спеціально для непорушних колекцій?
Доваль

1
ІМХО, ваші міркування щодо змінних інтерфейсів помилкові. Ви можете написати змінну реалізацію незмінних інтерфейсів, і тоді вона порушиться. Звичайно. Але це ваша вина, коли ви порушуєте договір. Просто перестань це робити. Це не відрізняється від розбиття SortedSetпідкласифікацією набору з невідповідною реалізацією. Або проходячи непослідовним Comparable. Якщо хочете, можна зламати майже все, що завгодно. Я здогадуюсь, що це означало @Doval під "подвійними стандартами".
maaartinus

2

Незмінні колекції можуть бути глибоко рекурсивними, порівняно з іншими, а також не безпідставно неефективними, якщо рівність об’єктів забезпечується secureHash. Це називається меркливим лісом. Це може бути в колекції або в їх частинах, як (самоврівноважуване бінарне) дерево AVL для відсортованої карти.

Якщо всі об'єкти java в цих колекціях не мають унікального ідентифікатора або деякого бітстрингу до хешу, колекція не має нічого хешувального, щоб однозначно називати себе.

Приклад: На моєму ноутбуці 4x1,6 ГГц я можу запустити 200K sha256s в секунду найменшого розміру, який підходить для 1 хеш-циклу (до 55 байт), порівняно з 500K HashMap Ops або 3M ops у хешбелі довгих. 200K / log (collectionSize) нових колекцій в секунду досить швидко для деяких речей, де важлива цілісність даних та анонімна глобальна масштабованість.


-3

Продуктивність. Колекції за своєю природою можуть бути дуже великими. Копіювання 1000 елементів у нову структуру з 1001 елементами замість того, щоб вставити один елемент, просто жахливо.

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

Зберігання. З незмінними об'єктами в багатопотоковому середовищі ви можете покласти десятки копій «одного» об’єкта в різні точки його життєвого циклу. Немає значення для об’єкта Календар або Дата, але коли його колекція 10 000 віджетів, це вб'є вас.


12
Колекції, що змінюються, потребують копіювання лише у тому випадку, якщо ви не можете поділитися ними через поширену здатність до зміни, як у Java. Паралельно, як правило, простіше з незмінними колекціями, оскільки вони не потребують блокування; а для наочності ви завжди можете мати змінне посилання на незмінну колекцію (поширену в OCaml). При спільному використанні оновлення можуть бути по суті безкоштовними. Ви можете зробити логарифмічно більше виділень, ніж зі змінною структурою, але після оновлення багато суб’єкти з минулим терміном дії можуть бути звільнені негайно або використані повторно, тому не обов'язково мати більшу пам'ять.
Джон Перді

4
Пара проблем. Колекції Clojure та Scala є незмінними, але підтримують легкі копії. Додавання елемента 1001 означає копіювання менше 33 елементів плюс створення декількох нових покажчиків. Якщо ви поділяєте колекцію, що змінюється, у різних потоках, під час її зміни виникають усілякі проблеми синхронізації. Такі операції, як "delete ()", кошмарні. Крім того, незмінні колекції можуть бути складені без змін, а потім скопійовані один раз у незмінну версію, безпечну для обміну по потоках.
GlenPeterson

4
Використання паралельності як аргументу проти незмінності є незвичним. Дублікати також.
Том Хотін - тайклін

4
Трохи пом'якшивши голоси щодо прихильності. ОП запитала, чому вони не застосовують незмінні колекції, і я надала розглянутий відповідь на питання. Імовірно, єдина прийнятна відповідь серед свідомих мод: "тому що вони помилилися". Насправді я маю певний досвід із цим, що потребує рефакторингу великих фрагментів коду, використовуючи інакше чудовий клас BigDecimal, виключно через погану продуктивність через незмінність у 512 разів, ніж використання подвійного плюс дещо возитися, щоб виправити десяткові місця.
Джеймс Андерсон

3
@JamesAnderson: Мої проблеми з вашою відповіддю: "Продуктивність" - ви можете сказати, що колекції, що змінюються в реальному житті, завжди реалізують певну форму спільного використання та повторного використання, щоб уникнути саме описаної вами проблеми. "Паралельність" - аргумент зводиться до "Якщо ви хочете змінити, то незмінний об'єкт не працює". Я маю на увазі, що якщо існує поняття "остання версія тієї ж речі", то щось потрібно мутувати, або сама річ, або щось, що належить їй. А в "Зберіганні" ти, здається, кажеш, що змінити іноді не бажано.
Джомінал
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.