Как стать автором
Обновить

Неизменяемых коллекций в Java не будет – ни сейчас, ни когда-либо

Время на прочтение9 мин
Количество просмотров22K
Всего голосов 25: ↑21 и ↓4+17
Комментарии116

Комментарии 116

Таким образом, получается, что использовать ImmutableList можно только локально, поскольку он передает границы API как List

Что-что он делает с границами API?

passes API boundaries as a List — переходит через границы API как List
Вот да, если перевод книги будет как в посте, то он нафиг не сдался. Пост на Хабре, наверняка, прошёл рецензирование редакторами.
Раньше приобретал у них книги и норм было, но вот по андроиду книжка (не сказать что прям новая, но и не древняя) — переведена очень странно. Например класс А субклассирует класс Б — означает что А унаследован от Б. И еще много подобного было. Несмотря на всю мою нелюбовь к чтению на английском — иногда подгорает и задумываешься о том чтобы в оригинале читать.

Так может в оригинале было "A subclasses B"?

Вполне возможно. Вот только вопрос, кто то в реальной жизни говорит на русском «А субклассирует Б»?

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


Переводить "subclasses" как "наследует" было бы можно, если бы не существовало английского термина "inherits". Но он есть, так что "наследует" уже занято. "Субклассирует" — единственный вариант.

«является подклассом»?

Всё равно отсебятина с потерей смысла. Если бы авторы хотели сказать, что "А является подклассом Б", то написали бы "A is a subclass of B". Но они написали иначе.

Но если добавить "прим. перев." с указанием, что в оригинале было "subclasses", то я готов такой перевод принять как допустимый.

А вы знаете смысл?

Точный смысл, позволяющий отличить один синоним от другого? Нет, но мне не трубуется знать в чём именно разница между волшебником и магом, чтобы быть уверенным, что "wizard" нельзя переводить как "маг", а "mage" — как "волшебник".

Потому что словарь не специализированный. Специализированный для этой области скорее всего не существует.

Литературный перевод без некоторой доли отсебятины невозможен, иначе получится простой дословный перевод.

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


Безусловно, есть особые ситуации, когда либо литературный перевод, либо дословность: некоторые шутки, специфический культурный контекст и т. д. Есть также известные затруднительные ситуации, когда есть разные "системы" перевода (пример — перевод англоязычных ругательств на русский язык). Но как только одна из конкурирующих систем выбрана, возможности для творчества остаётся немного.


Но это всё же особые случаи. В общем случае, повторю, неочевидно, почему литературность должна исключать дословность.

А есть техническая разница между subclasses и inherits?

Ну, по-идее "inherits" может применять и к интерфейсу. Но вообще это не важно. Я не уверен, есть ли разница между волшебником и магом, но если там, где в оригинале стоит "wizard", в переводе "маг", я говорю — в топку такой перевод.

Есть. Маленькая. Наследуются и поля, и методы. Subclasses же явно указывает на класс и только класс. Значит точный синоним — расширяет (extends). Вот и вся разница. Как между рыбой и сомом. :)
Не понял аргумент.
Если бы было «Наследуются и классы, и (допустим) модули», то понятно, что subclasses применяется только к первому.

Но наследуемые поля и методы одинаково наследуются внутри класса, так какая разница, subclasses или inherits, с этой точки зрения.
Сом — рыба, но не вся рыба — сом. Subclasses наследование, но не всё наследование — subclasses. Указание на классы в самом слове.
В ООП кроме наследования классов нет другого наследования. Поэтому синонимы.
В самом ООП — да, конечно, нельзя просто оторвать метод от класса или поле и куда-то пронаследовать, не потянув так или иначе класс. Но речь ведь идёт о переводе, о естественном языке. Можно сказать, что наследуется такой-то метод, а другой — нет. Можно сказать, что наследуется такое-то поле (protected), а private нет. Ну и интерфейсы наследуются.
В контексте обсуждаемого перевода, если написать «класс А наследуется от Б» вместо «класс А субклассирует класс Б», никакой потери точности нет. Но кто-то выше доказывает, что так писать нельзя, т.к. это не то, что хотел сказать автор. Собственно, именно это тут обсуждается.

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

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

Ну согласитесь, что нет такого слова «субклассирует» в русском языке. И звучит оно странно. А если повторить пять раз, то как-то даже смешно :) Думаю, что калька уместна в отсутствие слова или фразы, столь же полно передающих смысл исходного выражения.

Смысл оригинала переводчик не знает обычно. Только догадывается, как говорится, что хотел сказать автор.

Техническую литературу должен переводить специализированный переводчик со знанием предметной области. Если это не так, я бы не стал покупать такие книги.
Я изредка таки слышу вживую «A сабклассит B». Все понимают, что это жестокий жаргон, но для простоты и скорости… есть любители такого.
Но не «субклассирует», это перебор :\
Дельная мысль вынесена в заголовок статьи: неизменяемые коллекции не нужны. Нужны аннотации, что какой-то метод не меняет коллекцию.

В .NET с этим разобрались через интерфейсы (IReadOnlyList, IReadOnlyCollection и т.п.). Если не хочешь, что-то кто-то твой List менял, отдаёшь его всем потребителям как IReadOnlyList. Все ф-ции, которым не надо менять список, принимают IReadOnlyList, в них можно легко передать любой List. Таким образом, программист сам следит, где данные можно менять, а система типов ему помогает.
Так в статье как раз и написано про IReadOnlyList, только называют его UnmodifiableList.
Идея в том, что нет такой коллекции IReadOnlyList, это всего лишь интерфейс, который реализует в том числе обычный List. C интерфейсом нет протечки абстракций (если не делать cast к List, который в общем случае может выкинуть исключение, никто же не гарантирует что при передаче IReadOnlyList мы передали объект List).
Так и в джаве List — это интерфейс.
Но UnmodifiableList — нет.
Такого типа в джаве просто нет. Собственно, его автор и предлагает ввести.
Не спец в java, но в Collections есть такой код
    public static <T> List<T> unmodifiableList(List<? extends T> list) {
        return (list instanceof RandomAccess ?
                new UnmodifiableRandomAccessList<>(list) :
                new UnmodifiableList<>(list));
    }

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

Автор имеет в виду именно интерфейс, а не то что вы нашли.

Проблема в том что у этих классов все методы которые модифицируют коллекцию кидают UnsupportedOperationException, но они есть.

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

Тут я согласен с комментатором ниже: immutable и readonly — разные вещи. Readonly это как const в C++ и делать их отдельным классами не нужно (в разных частях программы к ним может быть разный доступ, так и const-ность можно снять всякими трюками).

А вот immutable, гарантирующие что при изменении коллекции будет создана копия, а по старой ссылке останется прежнее содержимое, дадут гарантии ненужности защитного копирования.

Поэтому ваше возражение немного мимо: я говорю, что отдельные классы для ReadOnly-коллекций не нужны, а вы говорите, что Immutable-коллекции нужны.

ReadOnly коллекции и Immatable коллекции — это принципиально разные вещи для разных применений. В .NET есть и то, и другое (System.Collections.Immutable).

Сомневаюсь в полезности UnmodifiableList в случае наличия ImmutableList. В вашем примере: public UnmodifiableList<Agent> teamRoster() — вот получили список, начали его на экран выводить, а он в это время поменялся из другой нити. Единственный вариант придумался, когда нужен именно UnmodifiableList без «стрельбы в ногу» — метод, циклически опрашивающий объекты на предмет какого-то события (пропустили один объект — ничего, на следующем цикле опросим; опросили один объект два раза за цикл — тоже ничего).

А вот что мешает изготовить свой ImmutableList и использовать в своих проектах? Не обязательно же всю экосистему менять. Получили от библиотеки List — скопировали сразу же содержимое в свой ImmutableList и дальше его гоняете. Лишнее копирование неприятно, но без него не обойтись.

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

Ну так и зачем он нужен, если есть ImmutableList? За исключением варианта с несколькими нитями, различий ImmutableList и UnmodifiableList не вижу.
Допустим, у вас есть класс A, который часто модифицирует некую коллекцию и иногда вызывает API для подсчёта статистики по этой коллекции. По хорошему, API должен гарантировать неизменность коллекции. Для этого, на вход следует принимать UnmodifiableList, чтобы декларировать неизменяемость.

ImmutableList в данном случае будет оверхэдом с точки зрения класса A, т.к. приводит к копированию массива или перестроению дерева (в зависимости от внутренней реализации ImmutableList-а) при каждой модификации коллекции.
Если, как предложено в статье, List является наследником UnmodifiableList, то получим ту же проблему, что в начале статьи описана: в API на входе UnmodifiableList, вызывают его с экземпляром List, тогда реализация API внутри может спокойно сделать приведение типов к List и менять, что захочет. Альтернативы — либо копирование, либо обёртка (как сейчас и сделано в Collections.unmodifiableList()). В обоих случаях оверхеда не избежать. Обёртка на первый взгляд кажется меньшим оверхедом, но если вспомнить про сборку мусора, то разница может оказаться совсем небольшой.
Ну мы же говорим про программирование, а не про то, как можно хакнуть систему. Если разработчики API опустились до того, чтобы кастить интерфейсы к конкретной имплементации (это должно быть уголовно наказуемо), то что им стоит через рефлекшн изменить private поля ImmutableList'а и добавить туда свой элемент?
Конечно, UnmodifiableList — более слабая абстракция, чем ImmutableList, но, учитывая накладные расходы и здравый смысл разработчиков, она всё же имеет место быть. Как уже было отмечено выше, в .NET-е уже давно есть IReadOnlyList, а относительно недавно появился и IImmutableList, и они отлично уживаются вместе, и никакие API не требуют строго IImmutableList на входе — чаще всего, ограничиваются IReadOnlyList (или ещё более ослабляют контракт до IReadOnlyCollection или даже до IEnumerable). Если вы не доверяете тому API, что вызываете, то всегда можно передать туда не ArrayList, а ImmutableList, и тогда этот злой API не сможет его скастить в List.
Бывают неприятные случаи, когда приходится хакать. Для этих случаев есть рефлекшены. Но к ним обычно и отношение соответствующее: «это ружьё рано или поздно стрельнёт в ногу». А приведение типов — вполне штатная операция, выполняемая на каждом углу, её легко можно написать, не особо задумываясь или «временно для тестов, потом поправлю». И когда код читаешь, оно не особо в глаза бросается. А если уж сначала UnmodifiableList станет, например, переменной типа Object (всякое в жизни бывает), то в другом месте кастинг в List будет выглядеть вообще абсолютно невинно.
приведение типов — вполне штатная операция, выполняемая на каждом углу

Стараюсь всегда избегать в своем коде, как раз по причине того что в результате получаем нарушение контрактов. Да и код обрастает костылями в виде (kotlin)
when(obj) {
    is ClassA -> ...
    is ClassB -> ...
    is ClassC -> ...
}

Да, иногда бывает что без каста не обойтись, но это то еще.
Ну это понятно, что по возможности стоит избегать. Но согласитесь, что привычный к написанию equals программист обычно воспринимает каст гораздо менее болезненно, чем рефлекшены.
Бывают неприятные случаи, когда приходится хакать. Для этих случаев есть рефлекшены.

Согласен, бывают. Но за всю мою 12-летнюю практику разработки софта мне лишь однажды пришлось хакнуть чужой софт через рефлекшн, и то лишь из-за того, что библиотека отрисовки графиков, которую использовали в моем проекте, не поддерживалась уже больше 5 лет, да и контора та давно закрылась.
Но мы всё же говорим о высоком. О языковых концепциях, и всё такое.
Так про то и речь, рефлекшены — это костыль для крайних случаев, и программисты (обычно) это понимают. А «штатными» средствами, считаю, не должно быть даже потенциальной возможности всё испортить. То есть, приведения типов MutableList <-> ImmutableList просто скомпилироваться не должны.
Честно говоря, не понимаю, почему некоторые видят в неизменяемых концепциях хоть какие-то плюсы. Большинство реализаций неизменяемой концепции будет медленнее изменяемой, иногда значительно. Так зачем тогда так упарываться и применять эту концепцию? А хорошему разработчику вообще пофик, изменяемые у него объекты или нет.
Неизменяемые типы гораздо удобнее в использовании в многопоточных приложениях. Меньше багов — дешевле поддержка. А хорошие программисты сами решат, где им лучше использовать (im-)mutable коллекции.
Чем удобней-то? Это все равно как сказать — вставать утром лучше с левой ноги.
Не надо локов. Все операции делаются через Interlocked.(Compare)Exchange, а значит, нет дедлоков. Я вообще забыл этот термин уже. Считаю, это плюс. Минус — memory footprint и время на GC. Поэтому, умные разработчики совмещают оба подхода.
— Как не поругаться с женой?
— Перестаньте с ней общаться.
— Минус: жена начинает готовить не то, что я хотел.
Да я уж понял, что вы больше про развитие стартапов, а не про программирование…
Извините, если не прав. Но с вашей стороны вообще никакой аргументации нет.
вы больше про развитие стартапов

Странная у Вас логика…
Отвечать на развёрнутый ответ анекдотом — тоже так себе логика.

Увеличение определённости и ясности кода — это хорошо. Если вы видите в коде неизменяемую сущность, вам больше не надо думать, что произойдёт, если она изменится. Понимание кода упрощается, уменьшается вероятность ошибки.

«Понимание кода упрощается, уменьшается вероятность ошибки.»
Что-то сомнительно, чтобы это было бы хоть немного заметно у более-менее опытного dev'а. А сама парадигма неизменяемости грозит жесткой просадкой скорости, потреблению памяти, лишним циклам GC.
Давайте начнём сначала и забудем ту дискуссию в соседней ветке. Вы можете привести какие-то доказательства жесткой просадки скорости? Есть какие-то статистические данные?

Кроме того, Вы, похоже, не поняли того, о чём сказал math_coder. Он не об Immutable объектах, а об усилении контракта.
доказательства жесткой просадки скорости

Так сама концепция неизменяемости тащит за собой накладные расходы в виде копирования объектов или лишних циклов оптимизатора, это все не бесплатно, чай не в сказке живете.
Есть даже какие-то бенчмарки по таким объектам из Guava, правда старенькие:
github.com/google/guava/issues/1268

об усилении контракта

Что за контракт такой?
Это всё происходит из-за того, что JVM не знает, какие объекты изменяемы, а какие нет. Из-за этого, GC обязан пройти по всем объектам, чтобы понять, какие из них ещё живы (классический Mark and Sweep + текущие модификации). В языках типа Haskell, которые заточены на работу с неизменяемыми данными, GC работает совсем по-другому: если он видит корневой объект, и знает, что этот объект immutable, он не будет проходить по дереву, т.к. и так понятно, что дочерние объекты тоже immutable. .NET сейчас тоже вводит т.н. record types, или readonly structs. Когда-нибудь, возможно, подтюнят и GC, чтобы не обходил всё дерево, если видит, что структура readonly.
Возможно, и до джавы дотянется тренд.

Что за контракт такой?

Есть такое понятие: контракт класса/интерфейса.
Обычно под контрактом подразумевается публичный контракт, хотя есть и контракты для дочерних классов, для классов внутри одного пакета, и ещё много разных вариантов.
Так вот, публичный «контракт» — это, условно, API класса. Когда класс говорит:
у меня есть метод Foo, который на вход принимает SomeWeakInterface, то это контракт.
А когда этот класс говорит, что метод Foo теперь принимает SomeStrongInterface, где SomeStrongInterface наследуется от SomeWeakInterface, то это называется «усиление контракта».
В языках типа Haskell, которые заточены на работу с неизменяемыми данными, GC работает совсем по-другому: если он видит корневой объект, и знает, что этот объект immutable, он не будет проходить по дереву, т.к. и так понятно, что дочерние объекты тоже immutable
Допустим, у меня 2 immutable словаря и по 5000 бакетов в каждом, т.е. всего в куче 10002 объектов. Каким образом при попадании в мусор первого словаря (потери на него всех ссылок), GC сможет выделить его 5000 бакетов, не проходя по дереву второго словаря? Если корневая ссылка ровно одна — объекты второго, ещё живого, словаря.
НЛО прилетело и опубликовало эту надпись здесь
Словари никак не связаны. Задача — объяснить, как работает механизм
если GC видит корневой объект, и знает, что этот объект immutable, он не будет проходить по дереву, т.к. и так понятно, что дочерние объекты тоже immutable
НЛО прилетело и опубликовало эту надпись здесь
А как узнать, какие объекты первого словаря можно прибить, не проходя по всем объектам второго? Обычная сборка мусора — это прохождение по всем корням до листьев, и всё, что не посещено, удаляется.
НЛО прилетело и опубликовало эту надпись здесь
Я не понимаю, что вы хотите сказать.

Было 2 иммутабельных словаря. Совершенно разных, никак не связанных. На один словарь теряем ссылку и все его объекты становятся мусором.

Может ли GC при удалении этих объектов как-то использовать факт, что словари были иммутабельными? Как мне кажется, нет. GC должен пройти по всем объектам второго, ещё живого, словаря, пометить их как живые, после чего всё непомеченное удалить. Иммутабельность тут никак не помогает.
НЛО прилетело и опубликовало эту надпись здесь
Вот граф объектов в памяти:
Скрытый текст


После присваивания dict2=null объекты b1, b2 становятся мусором.

Вопрос: как GC доберётся до b1 и b2, чтобы пометить память как свободную, кроме варианта пройти по всем корням (от dict1 до hashtable1, a1, a2, a3), пометить всё пройденное как живое, а всё остальное вернуть в пул свободной памяти?
НЛО прилетело и опубликовало эту надпись здесь
Поэтому мы просто берём и рекурсивно удаляем все объекты, доступные из dict2.
В какой именно момент?

Программа например, выполняет
dict2 = new hashtable; // создан hashtable2
dict2 = new hashtable; // создан hashtable3
dict2 = new hashtable; // создан hashtable4
dict2 = new hashtable; // создан hashtable5

… упс, тут память кончилась, запускается GC
как GC получит ссылки на все созданные hashtable2,3,4,5, чтобы их удалить?
НЛО прилетело и опубликовало эту надпись здесь
То есть, предлагается все аллокации записывать в некий стек, чтобы знать, что последний выделенный объект был hashtable5?

Хорошо. Берём со стека последний аллоцированный объект. И как мы узнаем, что его можно удалять? Нужно походить от всех корней, и если мы к нему не пришли, то можно удалять. Затем выталкикаем со стека следующий объект и снова от всех корней делаем обход? А не слишком ли большая сложность?

А если hashtable5, как в моём примере, ещё доступен? Всё, оптимизированный алгоритм останавливается? hashtable4 мы со стека не берём, чтобы удалить его вместе со всеми под-объектами, не проверяя ссылки? Нет гарантий, что из hashtable5 нет ссылок на внутренности hashtable4.
НЛО прилетело и опубликовало эту надпись здесь
заметим, что в этом случае мы просто могли бы сделать обычный copying GC из нулевого поколения (в котором все эти hashtableN, N = { 2… 5 } предположительно живут) в более старшее поколение (и не скопировалось бы ровным счётом ничего, так как все сдохли)
Но если в это же поколение попал hashtable1, он и все его под-объекты нужно копировать. Для этого их надо рекурсивно обойти, что противоречит тезису, что обход по внутренностям живых объектов не нужен.
НЛО прилетело и опубликовало эту надпись здесь
Тогда корректно сказать: Обход по внутренностям всех живых объектов нужен не на каждой итерации GC.

При перемещении gen0 в gen1 — не нужен. Но при сборке мусора в gen1 от него не избавишься.
НЛО прилетело и опубликовало эту надпись здесь
Оптимизации интересные, но они подразумевают, что мы знаем, какой объект (не только в куче, ещё и корни) создан раньше, какой позже. Если назначать версии из какого-то глобального счётчика, он будет узким местом в многопоточной среде.

Для счетчика можно использовать и atomic, который будет монотонно расти даже в многопоточный среде

Слишком большой contention выйдет для аллокаций.

НЛО прилетело и опубликовало эту надпись здесь
Тогда нельзя передавать ссылки между потоками, иначе будут перекрёстные ссылки между объектами в разных чанках. Это убивает преимущества иммутабельности в многопоточной среде, т.к. по сути получается N изолированных однопоточных процессов, не требующих синхронизации, а значит, можно писать в простом императивном стиле, не опасаясь гонок.
Вот отличная статья про оптимизации GC. В том числе, оптимизации, связанные с Immutable Objects. www.ibm.com/developerworks/library/j-jtp01274
Там нет ответа на вопрос ))) Если словарь (неважно, mutable или immutable) размещён в новом поколении GC, он будет обойден полностью. Если размещён в старом поколении и умер, то для освобождения памяти все словари того же поколения тоже будет обойдены полностью, но это делается реже (и это тоже одинаково для mutable или immutable).

Мусор, создаваемый immutable объектами лучше группируется по поколениям, из-за чего его проще собирать. Но самого мусора создаётся больше. Наверное, то на то и выходит.
Также легко представить ситуацию, когда mutable коллекция при изменении вообще не даёт нагрузку на GC: если по ключу меняется не ссылка, а примитивное поле (например, Integer) — граф объектов не меняется никак и объекты уходят в старое поколение и больше не требуют внимания GC.

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

Так дело даже не в этом. Чтобы оно хоть как-то шевелилось с приемлемой скоростью, Immutable должно быть базовой концепцией, как в том же Erlang. Микшировать два подхода в одном языке заранее обречено. Ну и для большинства применений(без такой жесткой многопоточности) концепция Immutable совершенно избыточна и бесполезна.
Что-то сомнительно, чтобы это было бы хоть немного заметно у более-менее опытного dev'а.

Ага, видел я такое воочию и не раз.


Сначала опытный dev не хочет озаботиться выражением контракта в коде. ("Ведь любому более-менее опытному разработчику должно быть понятно, что этот объект надо склонировать, а не менять внутреннее состояние существующего.")


А потом менее опытный dev, или даже опытный, но сосредоточенный мыслями на совсем других проблемах, допускает ошибку. А потом, через пару лет, обнаруживается баг, и оказывается, что важная система работает чудом, усиленно мутируя объекты, от которых этого не ожидалось. Ну и, соответственно, работает это всё плохо, а как исправлять теперь уже непонятно.

У Вас странные понятия об опытном dev'е. Нечего больше добавить.
НЛО прилетело и опубликовало эту надпись здесь

Почему они будут медленнее? Медленней они будут только в случае попыток "имитировать" мутабельность через возврат "сеттерами" нового значения. А вот в случае необходимости детектирования изменений оно будет на порядки быстрее — только ссылку сравнить, чтобы убедиться, что ничего не менялось.


А хороший разработчик знает, что чаще всего есть вещи важнее скорости.

На простом примере:
есть гиг данных, Вы заменяете в нем пару байтов. В случае Immutable объекта, он будет закопирован в новый, вместо того, чтобы просто поменять пару байтов. А если таких операций миллионы?
Immutable вообще не должен предоставлять возможности себя менять имхо(пусть и с копированием в другой объект), если объект менять нужно — так и делайте его изменяемым. А если прям вот хочется — то пусть разработчик сам извне создает новый объект, ясно осознавая что он копирует к себе данные из старого.
Это плохой пример, как делать не надо. Точно также можно говорить, что массивы не нужны, потому что вставка в середину требует копирования остатка массива.

Применение immutable оправдано, когда стоимость изменения посчитана (например, для дерева из N вершин модификация любой вершины потребует только log(N) копирований), и эта стоимость ниже альтернативного решения (например, блокировок потоков).
НЛО прилетело и опубликовало эту надпись здесь
  • ну и что, если это решает другие проблемы более важные чем гиг оперативки, например позволяет неограниченно горизонтально масштабироваться?
  • многие такие случаи реальные трансляторы хорошо поддерживающие иммутабельность оптимизирующий под капотом в мутабельность. Грубо говоря, бинарники одинаковые, но программист лишён возможности случайно мутировать объект, а потом, например, забыть сохранить изменения потому что сравнивал по ссылке, а не по значениям.

Плюсы не в неизменяемости как таковой, а в возможности отделения неизменяемых данных и чистых функций от изменяемых данных и деструктивных функций.
Апелляция к "хорошим разработчикам" ничем тут не отличается от истории про "настоящих шотландцев".

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

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


Если понимание кода упрощается — и опытные разработчики будут быстрее его понимать и могут потратить больше времени на добавление фичи. Если уменьшается вероятность ошибки — и опытный разработчик с меньшей вероятностью допустит ошибку (что вдвойне ценно, поскольку цена ошибки опытного разработчика обычно выше, чем цена ошибки неопытного).

Да, ещё момент — для иллюстрации исторической перспективы.
В каких-то первых версиях Фортрана можно было написать что-то вроде 2 = 3, после чего в программе дальше 2 было равно 3, потому что числовые константы связывались намертво с какой-то ячейкой памяти, и её можно было переписать.
Потом, если посмотреть старые фортрановские программы, там часто одна и та же переменная внутри функции имеет разный смысл на разных этапах — экономили память.
Сейчас первое вообще сложно вообразить, а за второе можно отхватить канделябром.
Иммутабельность и явные пометки, на каких переменных предполагаются изменения, а какие программист не собирается трогать — это естественный следующий шаг.

А почему автор прицепился именно к ImmutableList, если изменяемые методы есть уже у Collection? Что насчет UnmodifiableSet? Поэтому вместо UnmodifiableList нужен суперинтерфейс UnmodifiableCollection:
interface Collection extends UnmodifiableCollection

> // есть хорошие шансы, что `Iterable`
> // будет достаточно, но давайте предположим, что нам на самом деле нужен список
> public void payAgents(List agents)

Кстати, Iterable, от которой наследуется Collection не является immutable, т.к. возвращаемый Iterator содержит метод remove().

Вобщем, сделать можно, но при этом придется перелопатить всю java util, добавляя Unmodifiable-суперинтерфейсы. Наиболее интересен будет детальный анализ того, что при этом отвалится.

> // лично мне больше нравится возвращать потоки,
> // так как они немодифицируемые, но `List` все равно более распространен
> public List teamRoster() { }

Я видел, как многие так делают, но это ОООчень плохая идея, ибо потоки предназначены для единственного «прогона». При повторном «прогоне» вылезет:
java.lang.IllegalStateException: stream has already been operated upon or closed

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

Можно например сделать как поступили в Kotlin-е: в байткоде оперировать исключительно старыми добрыми коллекциями, а immutable сделать фичей исключительно компилятора.

> Вы можете себе представить, насколько монументальной и фактически бесконечной была бы такая задача?!

Вот не факт. Как только в Java введут новые коллекции, фреймворки быстро возьмут их на вооружение. Со стримами же как-то разобрались…

По поводу байт-кода — его можно переделывать при загрузке в jvm. Я так менял доступ к полю на вызов геттера, переделывал иерархию наследования, даже выносил методы в отдельные независимые интерфейсы… Так что это просто ещё одна решаемая проблема, а не какой-то стоп-фактор.

Скорее это просто дополнительные расходы, а не проблема. Причём чем популярнее будет такой подход, тем меньше расходы.

В нашем проекте мы решаем эту проблему как в питоне — джентльменским соглашением. Считается что List всегда неизменяемый, если только он не создан локально в текущем методе и тогда лучше пользоваться явным типом ArrayList например. Стараемся не передавать в методы изменяемые коллекции, но если очень надо передаём лямбду List::add а принимаем консьюмер. Конечно хуже, чем в Rust, но кажется самый адекватный выход.

в Python есть tuple и frozenset. Насколько я знаю, второй используется значительно реже, но его применение всё-таки оправдано (я сам его использовал крайне редко). Соглашения о совместимости есть в соглашениях collections.abc, если нужна проверка типов.

Все написано правильно. Но говоря по прикладном программировании я очень настороженно отношусь к реализации своих надстроек над стандартными классами. Причин тут несколько
1. Через 4 года, когда создатель этой надстройки окэшит опцион и пойдет работать в другую компанию — пришедшему программисту достанется еще одна загадка виде «зачем это все придуманно?». Статью на хабе он гарантированно не прочитает и будет использовать фичу как Бог на душу положит
2. Надстройка гарантированно не будет применяться консистентно в течении времени жизни проекта, что добавить +1 к запутанности проекта.
3. Имутабельность списков важна, и минимизация контрактов между методами и классами тоже крайне важна. Но часто компактный и простой код, который можно легко изменить значительно важнее. Т.е. если можно сделать код в 3 раза меньше за счет использования bare Java + одной/двух абсолютно стандартных библиотек, то я предпочту меньше кода.

UPD В текущем проекте вижу библиотечный класс FastByteArray2 созданный в 2004 году — все никак не соберусь с духом заглянуть, что внутри

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


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


Например, как сделать иммутабельным XML-документ?


var xml = xmlParser.parse(input);
var immutableXml = something(xml);  // ??? где-нибудь такое есть?
не упомянут важный момент, что иммутабельные объекты не всегда могут быть иммутабельными сразу, ведь их же еще надо как-то собрать
Это минимальная проблема. Все данные сделать приватными, передавать их в параметры конструктора. Методов на запись полей (кроме конструктора) не создавать.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий