Всем привет!

Сегодня вашему вниманию предлагается перевод вдумчиво написанной статьи об одной из базовых проблем Java — изменяемости, и о том, как она сказывается на устройстве структур данных и на работе с ними. Материал взят из блога Николая Парлога (Nicolai Parlog), чей блестящий литературный стиль мы очень постарались сохранить в переводе. Самого Николая замечательно характеризует отрывок из блога компании JUG.ru на Хабре; позволим себе привести здесь этот отрывок целиком:


Николай Парлог — такой масс-медиа чувак, который делает обзоры на фичи Java. Но он при этом не из Oracle, поэтому обзоры получаются удивительно откровенными и понятными. Иногда после них кого-то увольняют, но редко. Николай будет рассказывать про будущее Java, что будет в новой версии. У него хорошо получается рассказывать про тренды и вообще про большой мир. Он очень начитанный и эрудированный товарищ. Даже простые доклады приятно слушать, всё время узнаёшь что-то новое. При этом Николай знает за пределами того, что рассказывает. То есть можно приходить на любой доклад и просто наслаждаться, даже если это вообще не ваша тема. Он преподаёт. Написал «The Java Module System» для издательства Manning, ведёт блоги о разработке ПО на codefx.org, давно участвует в нескольких опенсорсных проектах. Прямо на конференции его можно нанять, он фрилансер. Правда, очень дорогой фрилансер. Вот доклад.

Читаем и голосуем. Кому пост особенно понравится — рекомендуем также посмотреть комментарии читателей к оригиналу поста.

Изменяемость – это плохо, так? Соответственно, неизменяемость – это хорошо. Основные структуры данных, при использовании которых неизменяемость оказывается особенно плодотворной, это коллекции: в Java это список (List), множество (Set) и словарь (Map). Однако, хотя JDK поставляется с неизменяемыми (или немодифицируемыми?) коллекциями, системе типов об этом ничего не известно. В JDK нет ImmutableList, и этот тип из Guava кажется мне совершенно бесполезным. Но почему же? Почему просто не добавить Immutable... в эту смесь и не сказать, что так и надо?

Что такое неизменяемая коллекция?


В терминологии JDK значения слов «неизменяемый» (immutable) и «немодифицируемый» (unmodifiable) за последние несколько лет изменились. Изначально «немодифицируемым» называли экземпляр, не допускавший изменяемости (мутабельности): в ответ на изменяющие методы он выбрасывал UnsupportedOperationException. Однако, его можно было менять по-другому – может быть, потому что он был просто оберткой вокруг изменяемой коллекции. Данные представления отражены в методах Collections::unmodifiableList, unmodifiableSet и unmodifiableMap, а также в их JavaDoc.

Поначалу термином "неизменяемые" обозначались коллекции, возвращаемые фабричными методами коллекций Java 9. Сами коллекции никаким образом нельзя было изменить (да, есть рефлексия, но она не считается), поэтому, представляется, что они оправдывают свое название. Увы, часто из-за этого возникает путаница. Допустим, есть метод, выводящий на экран все элементы из неизменяемой коллекции – всегда ли он будет давать один и тот же результат? Да? Или нет?

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

Неизменяемые коллекции, рассматриваемые в этой статье, могут содержать изменяемые элементы.

Лично мне не нравится такой пересмотр терминологии. На мой взгляд, термин «неизменяемая коллекция» должен означать лишь то, что сама коллекция не поддается изменениям, но никак не должен характеризовать содержащиеся в ней элементы. В таком случае есть и еще один положительный момент: термин «неизменяемость» в экосистеме Java не превращается в полную бессмыслицу.

Так или иначе, в этой статье мы поговорим о неизменяемых коллекциях, где…

  • Экземпляры, содержащиеся в коллекции, определяются на этапе работы конструктора
  • Этих экземпляров – ровное количество, ни убавить, ни прибавить
  • Не делается никаких утверждений относительно изменяемости этих элементов

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

Приступаем к добавлению неизменяемых коллекций!

Создадим интерфейс ImmutableList и сделаем его, относительно List, эээ…, чем? Супертипом или субтипом? Давайте остановимся на первом варианте.



Красиво, у ImmutableList нет изменяющих методов, поэтому использовать его всегда безопасно, так? Так?! Нет-с.

List<Agent> agents = new ArrayList<>();
// компилируется, поскольку `List` расширяет `ImmutableList`
ImmutableList<Agent> section4 = agents;
// ничего не выводит
section4.forEach(System.out::println);
 
// теперь давайте изменим `section4`
agents.add(new Agent("Motoko");
// выводит "Motoko" – обождите, через какую дырку она сюда вкралась?!
section4.forEach(System.out::println);

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

Хорошо, тогда ImmutableList расширяет List. Может быть?



Теперь, если API ожидает неизменяемый список, то именно такой список он и получит, но здесь есть два недостатка:

  • Неизменяемые списки все равно должны предлагать изменяющие методы (так как они определены в супертипе), а единственная возможная реализация приводит к выбросу исключения
  • Экземпляры ImmutableList также являются экземплярами List и при присвоении такой переменной, передаче в виде такого аргумента или возвращении такого типа логично предположить, что изменяемость разрешена.

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

Именно это я и имел в виду, говоря, что тип ImmutableList из Guava практически бесполезен. Это отличный образчик кода, очень надежный при работе с локальными неизменяемыми списками (поэтому я им активно пользуюсь), но, прибегая к нему, очень легко выйти за пределы неприступной, гарантированно компилируемой цитадели, стены который были сложены из неизменяемых типов – и только в таком виде неизменяемые типы могут полностью раскрыть свой потенциал. Это лучше, чем ничего, но неэффективно в качестве решения на уровне JDK.

Если ImmutableList не может расширять List, а обходной путь все равно не работает, то как вообще предполагается заставить все это работать?

Неизменяемость – это фича


Проблема, с которой мы столкнулись при первых двух попытках добавить неизменяемые типы, заключалась в нашем заблуждении, будто неизменяемость – это просто отсутствие чего-то: берем List, удаляем из него изменяющий код, получаем ImmutableList. Но, на самом деле, все это не так работает.

Если просто удалить изменяющие методы из List, то у нас получится список, доступный только для чтения. Либо, придерживаясь сформулированной выше терминологии, его можно назвать UnmodifiableList – он все-таки может меняться, просто менять его будете не вы.

Теперь мы можем добавить к этой картине еще две вещи:

  • Мы можем сделать его изменяемым, добавив соответствующие методы
  • Мы можем сделать его неизменяемым, добавив соответствующие гарантии

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

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

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



Хотя, я и не называл бы вещи именно этими именами, сама иерархия такого рода разумна. В Scala, например, практически так и делается. Разница заключается в том, что разделяемый супертип, который мы назвали UnmodifiableList, определяет изменяющие методы, возвращающие модифицированную коллекцию, а исходную оставляющие нетронутой. Таким образом, неизменяемый список получается персистентным и дает изменяемому варианту два набора изменяющих методов – унаследованный для получения модифицированных копий и свой собственный для изменений на месте.

Что же насчет Java? Можно ли модернизировать подобную иерархию, добавив в нее новые супертипы и сиблинги?

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

Самое классное в том, чтобы иметь UnmodifiableList, ImmutableList и List в качестве типов – в таком случае API смогут четко выражать, что им требуется, и что они предлагают.

public void payAgents(UnmodifiableList<Agent> agents) {
    // изменяющие методы для платежей не требуются, 
    // но и и неизменяемость не является необходимым условием
}
 
public void sendOnMission(ImmutableList<Agent> agents) {
    // миссия опасна (много потоков),
    // и важно, чтобы команда оставалась стабильной
}
 
public void downtime(List<Agent> agents) {
    // во время простоя члены команды могут уходить,
    // и на их место могут наниматься новые сотрудники, поэтому список должен быть изменяемым
}
 
public UnmodifiableList<Agent> teamRoster() {
    // можете просмотреть команду, но не можете ее редактировать,
    // а также не можете быть уверены, что ее не редактирует кто-нибудь еще
}
 
public ImmutableList<Agent> teamOnMission() {
    // если команда на задании, то ее состав не изменится
}
 
public List<Agent> team() {
    // получение изменяемого списка подразумевает, что список можно редактировать,
    // а затем просмотреть изменения в этом объекте
}

Однако, если только вы не начинаете проект с нуля, такой функционал, скорее всего, уже у вас будет, и выглядеть он будет примерно так:

// есть хорошие шансы, что `Iterable<Agent>`
// будет достаточно, но давайте предположим, что нам на самом деле нужен список 
public void payAgents(List<Agent> agents) { }
 
public void sendOnMission(List<Agent> agents) { }
 
public void downtime(List<Agent> agents) { }
 
// лично мне больше нравится возвращать потоки, 
// так как они немодифицируемые, но `List` все равно более распространен
public List<Agent> teamRoster() { }
 
// аналогично, это уже может быть `Stream<Agent>`
public List<Agent> teamOnMission() { }
 
public List<Agent> team() { }

Это нехорошо, так как, чтобы новые коллекции, только что введенные нами, были полезны, нам как бы нужно с ними работать! (уф). То, что приведено выше, напоминает код приложения, поэтому здесь напрашивается рефакторинг в сторону UnmodifiableList и ImmutableList, и осуществить его можно, как было показано в вышеприведенном листинге. Это может быть большой кусок работы, сопряженный с путаницей, когда нужно организовать взаимодействие старого и обновленного кода, но, как минимум, он кажется осуществимым.

Что же насчет фреймворков, библиотек и самого JDK как такового? Здесь все выглядит безрадостно. Попытка изменить параметр или возвращаемый тип с List на ImmutableList приведет к несовместимости с исходным кодом, т.e. существующий исходный код не скомпилируется с новой версией, так как эти типы не связаны друг с другом. Аналогично, при изменении возвращаемого типа с List на новый супертип UnmodifiableList приведет к ошибкам компиляции.

При введении новых типов потребуется вносить изменения и проводить перекомпиляцию в масштабах всей экосистемы.

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

  • Имени того класса, в качестве экземпляра которого объявлена цель
  • Имени метода
  • Типам параметров метода
  • Возвращаемому типу метода

Любое изменение в параметре или возвращаемом типе метода приведет к тому, что байт-код будет при ссылке на метод указывать неверную сигнатуру; в результате во время исполнения возникнет ошибка NoSuchMethodError. Если вносимое изменение совместимо с исходным кодом – например, если речь идет о сужении возвращаемого типа или расширения типа параметра – то перекомпиляции должно быть достаточно. Однако, при далеко идущих изменениях, например, при введении новых коллекций, все не так просто: чтобы такие изменения закрепились, нужно перекомпилировать всю экосистему Java. Это пропащее дело.

Единственный мыслимый способ воспользоваться такими новыми коллекциями, не нарушив совместимости – продублировать существующие методы каждый под новым именем, изменить API, после чего пометить старый вариант как нежелательный. Вы можете себе представить, насколько монументальной и фактически бесконечной была бы такая задача?!

Рефлексия


Конечно, неизменяемые типы коллекций – отличная штука, которую очень хотелось бы иметь, но мы вряд ли увидим что-то подобное в JDK. Грамотные реализации List и ImmutableList никогда не смогут расширять друг друга (на самом деле, оба они расширяют один и тот же списковый тип UnmodifiableList, доступный только для чтения), что затрудняет внедрение таких типов в существующие API.

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

Поэтому я считаю, что ничего подобного не произойдет – ни за что и никогда.

Комментарии (106)


  1. mayorovp
    04.10.2019 16:01
    +1

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

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


    1. mk2
      05.10.2019 00:41
      +1

      passes API boundaries as a List — переходит через границы API как List


      1. lam0x86
        05.10.2019 00:44
        +2

        Вот да, если перевод книги будет как в посте, то он нафиг не сдался. Пост на Хабре, наверняка, прошёл рецензирование редакторами.


        1. Neikist
          05.10.2019 11:35
          +1

          Раньше приобретал у них книги и норм было, но вот по андроиду книжка (не сказать что прям новая, но и не древняя) — переведена очень странно. Например класс А субклассирует класс Б — означает что А унаследован от Б. И еще много подобного было. Несмотря на всю мою нелюбовь к чтению на английском — иногда подгорает и задумываешься о том чтобы в оригинале читать.


          1. math_coder
            05.10.2019 11:52

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


            1. Neikist
              05.10.2019 11:54
              +1

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


              1. math_coder
                05.10.2019 12:03
                -1

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


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


                1. franzose
                  05.10.2019 12:16
                  +1

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


                  1. math_coder
                    05.10.2019 12:28

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


                    1. math_coder
                      05.10.2019 13:19

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


                    1. VolCh
                      05.10.2019 22:42

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


                      1. math_coder
                        06.10.2019 19:53
                        -1

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


                        1. VolCh
                          07.10.2019 19:42

                          Вы вот уверены, а составители различных словарей — нет. Вот например https://www.multitran.com/m.exe?s=wizard&l1=2&l2=1


                          1. math_coder
                            07.10.2019 21:16

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


                    1. sergey-b
                      06.10.2019 18:55

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


                      1. math_coder
                        06.10.2019 20:11
                        -1

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


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


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


                1. qw1
                  05.10.2019 12:43

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


                  1. math_coder
                    05.10.2019 13:04

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


                1. VolCh
                  05.10.2019 22:41

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


                  1. math_coder
                    06.10.2019 16:56
                    -2

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


                    1. franzose
                      07.10.2019 18:58

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


                    1. VolCh
                      07.10.2019 19:43

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


                      1. lam0x86
                        08.10.2019 00:14

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


              1. netch80
                05.10.2019 16:36

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


  1. qw1
    04.10.2019 17:46
    +1

    Дельная мысль вынесена в заголовок статьи: неизменяемые коллекции не нужны. Нужны аннотации, что какой-то метод не меняет коллекцию.

    В .NET с этим разобрались через интерфейсы (IReadOnlyList, IReadOnlyCollection и т.п.). Если не хочешь, что-то кто-то твой List менял, отдаёшь его всем потребителям как IReadOnlyList. Все ф-ции, которым не надо менять список, принимают IReadOnlyList, в них можно легко передать любой List. Таким образом, программист сам следит, где данные можно менять, а система типов ему помогает.


    1. lam0x86
      04.10.2019 18:17
      +3

      Так в статье как раз и написано про IReadOnlyList, только называют его UnmodifiableList.


      1. qw1
        04.10.2019 19:23

        Идея в том, что нет такой коллекции IReadOnlyList, это всего лишь интерфейс, который реализует в том числе обычный List. C интерфейсом нет протечки абстракций (если не делать cast к List, который в общем случае может выкинуть исключение, никто же не гарантирует что при передаче IReadOnlyList мы передали объект List).


        1. lam0x86
          04.10.2019 19:25

          Так и в джаве List — это интерфейс.


          1. qw1
            04.10.2019 19:31

            Но UnmodifiableList — нет.


            1. lam0x86
              04.10.2019 19:34

              Такого типа в джаве просто нет. Собственно, его автор и предлагает ввести.


              1. qw1
                04.10.2019 19:38
                -1

                Не спец в java, но в Collections есть такой код

                    public static <T> List<T> unmodifiableList(List<? extends T> list) {
                        return (list instanceof RandomAccess ?
                                new UnmodifiableRandomAccessList<>(list) :
                                new UnmodifiableList<>(list));
                    }


                1. lam0x86
                  04.10.2019 19:40

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


                1. mayorovp
                  04.10.2019 19:41

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


                1. igormich88
                  04.10.2019 21:41

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


    1. mayorovp
      04.10.2019 18:22
      +2

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


      1. qw1
        04.10.2019 19:35

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

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

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


    1. math_coder
      04.10.2019 18:39
      +3

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


  1. zzzzzzzzzzzz
    04.10.2019 20:11

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

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


    1. lam0x86
      04.10.2019 20:28
      +1

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


      1. zzzzzzzzzzzz
        04.10.2019 22:23

        Ну так и зачем он нужен, если есть ImmutableList? За исключением варианта с несколькими нитями, различий ImmutableList и UnmodifiableList не вижу.


        1. lam0x86
          04.10.2019 22:31
          +1

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

          ImmutableList в данном случае будет оверхэдом с точки зрения класса A, т.к. приводит к копированию массива или перестроению дерева (в зависимости от внутренней реализации ImmutableList-а) при каждой модификации коллекции.


          1. zzzzzzzzzzzz
            05.10.2019 00:04

            Если, как предложено в статье, List является наследником UnmodifiableList, то получим ту же проблему, что в начале статьи описана: в API на входе UnmodifiableList, вызывают его с экземпляром List, тогда реализация API внутри может спокойно сделать приведение типов к List и менять, что захочет. Альтернативы — либо копирование, либо обёртка (как сейчас и сделано в Collections.unmodifiableList()). В обоих случаях оверхеда не избежать. Обёртка на первый взгляд кажется меньшим оверхедом, но если вспомнить про сборку мусора, то разница может оказаться совсем небольшой.


            1. lam0x86
              05.10.2019 00:24
              +1

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


              1. zzzzzzzzzzzz
                05.10.2019 09:23

                Бывают неприятные случаи, когда приходится хакать. Для этих случаев есть рефлекшены. Но к ним обычно и отношение соответствующее: «это ружьё рано или поздно стрельнёт в ногу». А приведение типов — вполне штатная операция, выполняемая на каждом углу, её легко можно написать, не особо задумываясь или «временно для тестов, потом поправлю». И когда код читаешь, оно не особо в глаза бросается. А если уж сначала UnmodifiableList станет, например, переменной типа Object (всякое в жизни бывает), то в другом месте кастинг в List будет выглядеть вообще абсолютно невинно.


                1. Neikist
                  05.10.2019 11:45
                  +1

                  приведение типов — вполне штатная операция, выполняемая на каждом углу

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

                  Да, иногда бывает что без каста не обойтись, но это то еще.


                  1. zzzzzzzzzzzz
                    05.10.2019 12:36

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


                1. lam0x86
                  06.10.2019 05:16

                  Бывают неприятные случаи, когда приходится хакать. Для этих случаев есть рефлекшены.

                  Согласен, бывают. Но за всю мою 12-летнюю практику разработки софта мне лишь однажды пришлось хакнуть чужой софт через рефлекшн, и то лишь из-за того, что библиотека отрисовки графиков, которую использовали в моем проекте, не поддерживалась уже больше 5 лет, да и контора та давно закрылась.
                  Но мы всё же говорим о высоком. О языковых концепциях, и всё такое.


                  1. zzzzzzzzzzzz
                    06.10.2019 10:02

                    Так про то и речь, рефлекшены — это костыль для крайних случаев, и программисты (обычно) это понимают. А «штатными» средствами, считаю, не должно быть даже потенциальной возможности всё испортить. То есть, приведения типов MutableList <-> ImmutableList просто скомпилироваться не должны.


  1. JordanoBruno
    05.10.2019 00:14

    Честно говоря, не понимаю, почему некоторые видят в неизменяемых концепциях хоть какие-то плюсы. Большинство реализаций неизменяемой концепции будет медленнее изменяемой, иногда значительно. Так зачем тогда так упарываться и применять эту концепцию? А хорошему разработчику вообще пофик, изменяемые у него объекты или нет.


    1. lam0x86
      05.10.2019 00:27

      Неизменяемые типы гораздо удобнее в использовании в многопоточных приложениях. Меньше багов — дешевле поддержка. А хорошие программисты сами решат, где им лучше использовать (im-)mutable коллекции.


      1. JordanoBruno
        05.10.2019 00:32

        Чем удобней-то? Это все равно как сказать — вставать утром лучше с левой ноги.


        1. lam0x86
          05.10.2019 00:39

          Не надо локов. Все операции делаются через Interlocked.(Compare)Exchange, а значит, нет дедлоков. Я вообще забыл этот термин уже. Считаю, это плюс. Минус — memory footprint и время на GC. Поэтому, умные разработчики совмещают оба подхода.


          1. JordanoBruno
            05.10.2019 00:57

            — Как не поругаться с женой?
            — Перестаньте с ней общаться.
            — Минус: жена начинает готовить не то, что я хотел.


            1. lam0x86
              05.10.2019 01:00

              Да я уж понял, что вы больше про развитие стартапов, а не про программирование…
              Извините, если не прав. Но с вашей стороны вообще никакой аргументации нет.


              1. JordanoBruno
                05.10.2019 01:10

                вы больше про развитие стартапов

                Странная у Вас логика…


                1. lam0x86
                  05.10.2019 01:16

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


    1. math_coder
      05.10.2019 01:32
      +1

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


      1. JordanoBruno
        05.10.2019 01:45

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


        1. lam0x86
          05.10.2019 01:53

          Давайте начнём сначала и забудем ту дискуссию в соседней ветке. Вы можете привести какие-то доказательства жесткой просадки скорости? Есть какие-то статистические данные?

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


          1. JordanoBruno
            05.10.2019 02:09

            доказательства жесткой просадки скорости

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

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

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


            1. lam0x86
              05.10.2019 02:33

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

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

              Есть такое понятие: контракт класса/интерфейса.
              Обычно под контрактом подразумевается публичный контракт, хотя есть и контракты для дочерних классов, для классов внутри одного пакета, и ещё много разных вариантов.
              Так вот, публичный «контракт» — это, условно, API класса. Когда класс говорит:
              у меня есть метод Foo, который на вход принимает SomeWeakInterface, то это контракт.
              А когда этот класс говорит, что метод Foo теперь принимает SomeStrongInterface, где SomeStrongInterface наследуется от SomeWeakInterface, то это называется «усиление контракта».


              1. qw1
                05.10.2019 08:46

                В языках типа Haskell, которые заточены на работу с неизменяемыми данными, GC работает совсем по-другому: если он видит корневой объект, и знает, что этот объект immutable, он не будет проходить по дереву, т.к. и так понятно, что дочерние объекты тоже immutable
                Допустим, у меня 2 immutable словаря и по 5000 бакетов в каждом, т.е. всего в куче 10002 объектов. Каким образом при попадании в мусор первого словаря (потери на него всех ссылок), GC сможет выделить его 5000 бакетов, не проходя по дереву второго словаря? Если корневая ссылка ровно одна — объекты второго, ещё живого, словаря.


                1. 0xd34df00d
                  05.10.2019 17:54

                  Я не до конца понял задачу. Как эти два словаря связаны? Один является версией другого?


                  1. qw1
                    06.10.2019 00:57

                    Словари никак не связаны. Задача — объяснить, как работает механизм

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


                    1. 0xd34df00d
                      06.10.2019 01:01

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


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


                      1. qw1
                        06.10.2019 10:08

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


                        1. 0xd34df00d
                          06.10.2019 16:42

                          Узнать, э, логически.


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


                          1. qw1
                            06.10.2019 20:57

                            Я не понимаю, что вы хотите сказать.

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

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


                            1. 0xd34df00d
                              06.10.2019 21:00

                              Может ли GC при удалении этих объектов как-то использовать факт, что словари были иммутабельными?

                              Да. Ему не надо проходить по всем объектам ещё живого словаря, а достаточно, условно, сравнить таймстамп корня ещё живого словаря с корнем удаляемого словаря. Если ещё живой словарь старше удаляемого словаря, то он гарантированно не может [транзитивно] ссылаться на что-либо из более молодого словаря, и по нему обходить не надо.


                              Чтобы получить чуть более хорошую интуицию, попробуйте придумать ситуацию, в которой этого недостаточно.


                              1. qw1
                                06.10.2019 21:14

                                Вот граф объектов в памяти:

                                Скрытый текст


                                1. 0xd34df00d
                                  06.10.2019 21:20

                                  Давайте рассмотрим предельный случай: с прошлой сборки мусора не было создано никаких объектов, кроме dict2 и его детей (hashtable2, b1, b2). В таком случае мы знаем, что никакие объекты, кроме dict2, не могут ссылаться на объекты, транзитивно доступные из dict2.


                                  Говоря чуть аккуратнее и точнее

                                  Мы знаем, что в подграф объектов, являющийся транзитивным замыканием отношения «имеет указатель на» и содержащий вершину dict2, нет рёбер из объектов вне этого подграфа.


                                  1. qw1
                                    06.10.2019 21:28

                                    Поэтому мы просто берём и рекурсивно удаляем все объекты, доступные из dict2.
                                    В какой именно момент?

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

                                    … упс, тут память кончилась, запускается GC
                                    как GC получит ссылки на все созданные hashtable2,3,4,5, чтобы их удалить?


                                    1. 0xd34df00d
                                      06.10.2019 21:34
                                      -1

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


                                      Тем не менее, хороший пример, давайте рассмотрим.


                                      Во-первых, в этом виде hashtable5 удалять нельзя, на него ещё dict2 ссылается. Я предположу, что вы там дальше написали dict2 = null (хотя тут уже от иммутабельности ничего не осталось, раз вы в одну и ту же переменную что-то присваиваете, но закроем глаза на это и будем считать, что вы просто 5 раз вызвали одну и ту же функцию).


                                      Во-вторых, как не сканировать всю кучу даже в этом случае? Начинаем с самого молодого объекта, это hashtable5. После него ничего не создано, на него ничего не ссылается, поэтому его можно рекурсивно удалить целиком и не ходить по другим объектам. Какой дальше самый молодой объект? hashtable4. Моложе него (теперь) никого нет, поэтому его тоже можно спокойно рекурсивно удалять.


                                      Ну и так далее.


                                      Кстати, заметим, что в этом случае мы просто могли бы сделать обычный copying GC из нулевого поколения (в котором все эти hashtableN, N = { 2… 5 } предположительно живут) в более старшее поколение (и не скопировалось бы ровным счётом ничего, так как все сдохли), но даже это, как мы видим, не нужно.


                                      1. qw1
                                        06.10.2019 22:33

                                        То есть, предлагается все аллокации записывать в некий стек, чтобы знать, что последний выделенный объект был hashtable5?

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

                                        А если hashtable5, как в моём примере, ещё доступен? Всё, оптимизированный алгоритм останавливается? hashtable4 мы со стека не берём, чтобы удалить его вместе со всеми под-объектами, не проверяя ссылки? Нет гарантий, что из hashtable5 нет ссылок на внутренности hashtable4.


                                        1. 0xd34df00d
                                          06.10.2019 22:52

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

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


                                          А если hashtable5, как в моём примере, ещё доступен? Всё, оптимизированный алгоритм останавливается? hashtable4 мы со стека не берём, чтобы удалить его вместе со всеми под-объектами, не проверяя ссылки? Нет гарантий, что из hashtable5 нет ссылок на внутренности hashtable4.

                                          Тогда делаем copying GC из gen0 в более старое поколение.


                                      1. qw1
                                        06.10.2019 22:35

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


                                        1. 0xd34df00d
                                          06.10.2019 22:55

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

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


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


                                          1. qw1
                                            06.10.2019 23:10

                                            Тогда корректно сказать: Обход по внутренностям всех живых объектов нужен не на каждой итерации GC.

                                            При перемещении gen0 в gen1 — не нужен. Но при сборке мусора в gen1 от него не избавишься.


                                            1. 0xd34df00d
                                              07.10.2019 00:39

                                              Да, именно так. По опыту в среднем приходится 500-10000 gen0-сборок на одну gen1-сборку. Ну и ещё есть compact regions, но то совсем отдельная история.


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


                                              1. qw1
                                                07.10.2019 10:24
                                                +1

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


                    1. lam0x86
                      06.10.2019 01:20

                      Вот отличная статья про оптимизации GC. В том числе, оптимизации, связанные с Immutable Objects. www.ibm.com/developerworks/library/j-jtp01274


                      1. qw1
                        06.10.2019 10:24

                        Там нет ответа на вопрос ))) Если словарь (неважно, mutable или immutable) размещён в новом поколении GC, он будет обойден полностью. Если размещён в старом поколении и умер, то для освобождения памяти все словари того же поколения тоже будет обойдены полностью, но это делается реже (и это тоже одинаково для mutable или immutable).

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


                      1. qw1
                        06.10.2019 10:37

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

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


              1. JordanoBruno
                05.10.2019 10:49

                Это всё происходит из-за того, что JVM не знает, какие объекты изменяемы, а какие нет.

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


        1. math_coder
          05.10.2019 03:45
          +2

          Что-то сомнительно, чтобы это было бы хоть немного заметно у более-менее опытного dev'а.

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


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


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


          1. JordanoBruno
            05.10.2019 10:51

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


        1. 0xd34df00d
          05.10.2019 17:53

          А сама парадигма неизменяемости грозит жесткой просадкой скорости, потреблению памяти, лишним циклам GC.

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


    1. VolCh
      05.10.2019 07:33

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


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


      1. JordanoBruno
        05.10.2019 10:54
        +1

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


        1. Neikist
          05.10.2019 11:51

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


        1. qw1
          05.10.2019 12:52

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

          Применение immutable оправдано, когда стоимость изменения посчитана (например, для дерева из N вершин модификация любой вершины потребует только log(N) копирований), и эта стоимость ниже альтернативного решения (например, блокировок потоков).


        1. 0xd34df00d
          05.10.2019 18:06

          Тогда вы неправильно выбрали структуру данных (либо ЯП, либо уровень абстракций).


          Тут есть два варианта:


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


        1. VolCh
          05.10.2019 22:50

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


    1. Pand5461
      05.10.2019 13:23

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


      1. JordanoBruno
        05.10.2019 14:38

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


        1. Pand5461
          05.10.2019 15:50

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


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


        1. Pand5461
          05.10.2019 16:13
          +1

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


  1. Throwable
    05.10.2019 14:02

    А почему автор прицепился именно к 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 введут новые коллекции, фреймворки быстро возьмут их на вооружение. Со стримами же как-то разобрались…


    1. Prototik
      05.10.2019 14:24

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


      1. VolCh
        05.10.2019 22:53

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


  1. zzzzzzzzzzzz
    05.10.2019 22:27
    +1

    По мотивам навеянных статьёй мыслей поигрался с кодом и написал статью-ответ.


  1. SharplEr
    06.10.2019 11:17
    +1

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


    1. eirnym
      06.10.2019 17:59

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


  1. Semenych
    06.10.2019 13:36

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

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


  1. sergey-b
    06.10.2019 19:11

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


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


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


    var xml = xmlParser.parse(input);
    var immutableXml = something(xml);  // ??? где-нибудь такое есть?


    1. qw1
      06.10.2019 21:01

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