Comments 116
Таким образом, получается, что использовать ImmutableList можно только локально, поскольку он передает границы API как List
Что-что он делает с границами API?
Так может в оригинале было "A subclasses B"?
Это вообще не аргумент. В реальной жизни много чего не говорят. Если устоявшегося перевода у термина нет, бывает, что приходится и новый термин вводить в оборот.
Переводить "subclasses" как "наследует" было бы можно, если бы не существовало английского термина "inherits". Но он есть, так что "наследует" уже занято. "Субклассирует" — единственный вариант.
«является подклассом»?
Всё равно отсебятина с потерей смысла. Если бы авторы хотели сказать, что "А является подклассом Б", то написали бы "A is a subclass of B". Но они написали иначе.
Но если добавить "прим. перев." с указанием, что в оригинале было "subclasses", то я готов такой перевод принять как допустимый.
А вы знаете смысл?
Точный смысл, позволяющий отличить один синоним от другого? Нет, но мне не трубуется знать в чём именно разница между волшебником и магом, чтобы быть уверенным, что "wizard" нельзя переводить как "маг", а "mage" — как "волшебник".
Вы вот уверены, а составители различных словарей — нет. Вот например https://www.multitran.com/m.exe?s=wizard&l1=2&l2=1
Литературный перевод без некоторой доли отсебятины невозможен, иначе получится простой дословный перевод.
Мне кажется, это как минимум не очевидно. Я думаю, дословный литературный перевод вполне возможен.
Безусловно, есть особые ситуации, когда либо литературный перевод, либо дословность: некоторые шутки, специфический культурный контекст и т. д. Есть также известные затруднительные ситуации, когда есть разные "системы" перевода (пример — перевод англоязычных ругательств на русский язык). Но как только одна из конкурирующих систем выбрана, возможности для творчества остаётся немного.
Но это всё же особые случаи. В общем случае, повторю, неочевидно, почему литературность должна исключать дословность.
Ну, по-идее "inherits" может применять и к интерфейсу. Но вообще это не важно. Я не уверен, есть ли разница между волшебником и магом, но если там, где в оригинале стоит "wizard", в переводе "маг", я говорю — в топку такой перевод.
Если бы было «Наследуются и классы, и (допустим) модули», то понятно, что subclasses применяется только к первому.
Но наследуемые поля и методы одинаково наследуются внутри класса, так какая разница, subclasses или inherits, с этой точки зрения.
Из того, что автор оригинала использует один из многих синонимов в своём языке, вовсе не следует, что переводчик должен вводить новый термин в целевом языке из-за того, что в нём число синонимов для понятия меньше.
Если хочет сохранить смысл оригинала — должен. Если хочет нести отсебятину — тогда он может вообще оставить только название, а остальное написать сам.
Ну согласитесь, что нет такого слова «субклассирует» в русском языке. И звучит оно странно. А если повторить пять раз, то как-то даже смешно :) Думаю, что калька уместна в отсутствие слова или фразы, столь же полно передающих смысл исходного выражения.
Смысл оригинала переводчик не знает обычно. Только догадывается, как говорится, что хотел сказать автор.
Но не «субклассирует», это перебор :\
В .NET с этим разобрались через интерфейсы (IReadOnlyList, IReadOnlyCollection и т.п.). Если не хочешь, что-то кто-то твой List менял, отдаёшь его всем потребителям как IReadOnlyList. Все ф-ции, которым не надо менять список, принимают IReadOnlyList, в них можно легко передать любой List. Таким образом, программист сам следит, где данные можно менять, а система типов ему помогает.
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
Автор имеет в виду именно интерфейс, а не то что вы нашли.
Вообще-то, неизменяемые коллекции как раз нужны. Будучи применёнными в нужном месте, они позволяют избежать многократного защитного копирования.
А вот immutable, гарантирующие что при изменении коллекции будет создана копия, а по старой ссылке останется прежнее содержимое, дадут гарантии ненужности защитного копирования.
Поэтому ваше возражение немного мимо: я говорю, что отдельные классы для ReadOnly-коллекций не нужны, а вы говорите, что Immutable-коллекции нужны.
ReadOnly коллекции и Immatable коллекции — это принципиально разные вещи для разных применений. В .NET есть и то, и другое (System.Collections.Immutable).
public UnmodifiableList<Agent> teamRoster()
— вот получили список, начали его на экран выводить, а он в это время поменялся из другой нити. Единственный вариант придумался, когда нужен именно UnmodifiableList без «стрельбы в ногу» — метод, циклически опрашивающий объекты на предмет какого-то события (пропустили один объект — ничего, на следующем цикле опросим; опросили один объект два раза за цикл — тоже ничего).А вот что мешает изготовить свой ImmutableList и использовать в своих проектах? Не обязательно же всю экосистему менять. Получили от библиотеки List — скопировали сразу же содержимое в свой ImmutableList и дальше его гоняете. Лишнее копирование неприятно, но без него не обойтись.
Смысл UnmodifiableList'а не в том, что он не может измениться извне, а в том, что передавая его в какой-то метод, ты можешь быть уверен, что после выхода из метода лист останется тем же.
ImmutableList в данном случае будет оверхэдом с точки зрения класса A, т.к. приводит к копированию массива или перестроению дерева (в зависимости от внутренней реализации ImmutableList-а) при каждой модификации коллекции.
List
является наследником UnmodifiableList
, то получим ту же проблему, что в начале статьи описана: в API на входе UnmodifiableList
, вызывают его с экземпляром List
, тогда реализация API внутри может спокойно сделать приведение типов к List
и менять, что захочет. Альтернативы — либо копирование, либо обёртка (как сейчас и сделано в Collections.unmodifiableList()
). В обоих случаях оверхеда не избежать. Обёртка на первый взгляд кажется меньшим оверхедом, но если вспомнить про сборку мусора, то разница может оказаться совсем небольшой.Конечно, UnmodifiableList — более слабая абстракция, чем ImmutableList, но, учитывая накладные расходы и здравый смысл разработчиков, она всё же имеет место быть. Как уже было отмечено выше, в .NET-е уже давно есть IReadOnlyList, а относительно недавно появился и IImmutableList, и они отлично уживаются вместе, и никакие API не требуют строго IImmutableList на входе — чаще всего, ограничиваются IReadOnlyList (или ещё более ослабляют контракт до IReadOnlyCollection или даже до IEnumerable). Если вы не доверяете тому API, что вызываете, то всегда можно передать туда не ArrayList, а ImmutableList, и тогда этот злой API не сможет его скастить в List.
приведение типов — вполне штатная операция, выполняемая на каждом углу
Стараюсь всегда избегать в своем коде, как раз по причине того что в результате получаем нарушение контрактов. Да и код обрастает костылями в виде (kotlin)
when(obj) {
is ClassA -> ...
is ClassB -> ...
is ClassC -> ...
}
Да, иногда бывает что без каста не обойтись, но это то еще.
Бывают неприятные случаи, когда приходится хакать. Для этих случаев есть рефлекшены.
Согласен, бывают. Но за всю мою 12-летнюю практику разработки софта мне лишь однажды пришлось хакнуть чужой софт через рефлекшн, и то лишь из-за того, что библиотека отрисовки графиков, которую использовали в моем проекте, не поддерживалась уже больше 5 лет, да и контора та давно закрылась.
Но мы всё же говорим о высоком. О языковых концепциях, и всё такое.
— Перестаньте с ней общаться.
— Минус: жена начинает готовить не то, что я хотел.
Увеличение определённости и ясности кода — это хорошо. Если вы видите в коде неизменяемую сущность, вам больше не надо думать, что произойдёт, если она изменится. Понимание кода упрощается, уменьшается вероятность ошибки.
Что-то сомнительно, чтобы это было бы хоть немного заметно у более-менее опытного dev'а. А сама парадигма неизменяемости грозит жесткой просадкой скорости, потреблению памяти, лишним циклам GC.
Кроме того, Вы, похоже, не поняли того, о чём сказал math_coder. Он не об Immutable объектах, а об усилении контракта.
доказательства жесткой просадки скорости
Так сама концепция неизменяемости тащит за собой накладные расходы в виде копирования объектов или лишних циклов оптимизатора, это все не бесплатно, чай не в сказке живете.
Есть даже какие-то бенчмарки по таким объектам из Guava, правда старенькие:
github.com/google/guava/issues/1268
об усилении контракта
Что за контракт такой?
Возможно, и до джавы дотянется тренд.
Что за контракт такой?
Есть такое понятие: контракт класса/интерфейса.
Обычно под контрактом подразумевается публичный контракт, хотя есть и контракты для дочерних классов, для классов внутри одного пакета, и ещё много разных вариантов.
Так вот, публичный «контракт» — это, условно, 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, как в моём примере, ещё доступен? Всё, оптимизированный алгоритм останавливается? hashtable4 мы со стека не берём, чтобы удалить его вместе со всеми под-объектами, не проверяя ссылки? Нет гарантий, что из hashtable5 нет ссылок на внутренности hashtable4.
заметим, что в этом случае мы просто могли бы сделать обычный copying GC из нулевого поколения (в котором все эти hashtableN, N = { 2… 5 } предположительно живут) в более старшее поколение (и не скопировалось бы ровным счётом ничего, так как все сдохли)Но если в это же поколение попал hashtable1, он и все его под-объекты нужно копировать. Для этого их надо рекурсивно обойти, что противоречит тезису, что обход по внутренностям живых объектов не нужен.
При перемещении gen0 в gen1 — не нужен. Но при сборке мусора в gen1 от него не избавишься.
Для счетчика можно использовать и atomic, который будет монотонно расти даже в многопоточный среде
Мусор, создаваемый immutable объектами лучше группируется по поколениям, из-за чего его проще собирать. Но самого мусора создаётся больше. Наверное, то на то и выходит.
И ту же ситуацию, с immutable коллекцией: при изменении коллекции будут создаваться новые копии, а старые элементы, которые давно лежали в старом поколении, превращаются в мусор и требуется уборка старого поколения.
Это всё происходит из-за того, что JVM не знает, какие объекты изменяемы, а какие нет.
Так дело даже не в этом. Чтобы оно хоть как-то шевелилось с приемлемой скоростью, Immutable должно быть базовой концепцией, как в том же Erlang. Микшировать два подхода в одном языке заранее обречено. Ну и для большинства применений(без такой жесткой многопоточности) концепция Immutable совершенно избыточна и бесполезна.
Что-то сомнительно, чтобы это было бы хоть немного заметно у более-менее опытного dev'а.
Ага, видел я такое воочию и не раз.
Сначала опытный dev не хочет озаботиться выражением контракта в коде. ("Ведь любому более-менее опытному разработчику должно быть понятно, что этот объект надо склонировать, а не менять внутреннее состояние существующего.")
А потом менее опытный dev, или даже опытный, но сосредоточенный мыслями на совсем других проблемах, допускает ошибку. А потом, через пару лет, обнаруживается баг, и оказывается, что важная система работает чудом, усиленно мутируя объекты, от которых этого не ожидалось. Ну и, соответственно, работает это всё плохо, а как исправлять теперь уже непонятно.
Почему они будут медленнее? Медленней они будут только в случае попыток "имитировать" мутабельность через возврат "сеттерами" нового значения. А вот в случае необходимости детектирования изменений оно будет на порядки быстрее — только ссылку сравнить, чтобы убедиться, что ничего не менялось.
А хороший разработчик знает, что чаще всего есть вещи важнее скорости.
есть гиг данных, Вы заменяете в нем пару байтов. В случае Immutable объекта, он будет закопирован в новый, вместо того, чтобы просто поменять пару байтов. А если таких операций миллионы?
Применение immutable оправдано, когда стоимость изменения посчитана (например, для дерева из N вершин модификация любой вершины потребует только log(N) копирований), и эта стоимость ниже альтернативного решения (например, блокировок потоков).
- ну и что, если это решает другие проблемы более важные чем гиг оперативки, например позволяет неограниченно горизонтально масштабироваться?
- многие такие случаи реальные трансляторы хорошо поддерживающие иммутабельность оптимизирующий под капотом в мутабельность. Грубо говоря, бинарники одинаковые, но программист лишён возможности случайно мутировать объект, а потом, например, забыть сохранить изменения потому что сравнивал по ссылке, а не по значениям.
Плюсы не в неизменяемости как таковой, а в возможности отделения неизменяемых данных и чистых функций от изменяемых данных и деструктивных функций.
Апелляция к "хорошим разработчикам" ничем тут не отличается от истории про "настоящих шотландцев".
Я понимаю, что я не слышал о людях, которые могут писать код совсем без ошибок. Если с добавлением в язык какой-то концепции класс ошибок можно будет отлавливать в ходе компиляции, а не на тестах / в продакшене — это же хорошо.
Если понимание кода упрощается — и опытные разработчики будут быстрее его понимать и могут потратить больше времени на добавление фичи. Если уменьшается вероятность ошибки — и опытный разработчик с меньшей вероятностью допустит ошибку (что вдвойне ценно, поскольку цена ошибки опытного разработчика обычно выше, чем цена ошибки неопытного).
Да, ещё момент — для иллюстрации исторической перспективы.
В каких-то первых версиях Фортрана можно было написать что-то вроде 2 = 3
, после чего в программе дальше 2 было равно 3, потому что числовые константы связывались намертво с какой-то ячейкой памяти, и её можно было переписать.
Потом, если посмотреть старые фортрановские программы, там часто одна и та же переменная внутри функции имеет разный смысл на разных этапах — экономили память.
Сейчас первое вообще сложно вообразить, а за второе можно отхватить канделябром.
Иммутабельность и явные пометки, на каких переменных предполагаются изменения, а какие программист не собирается трогать — это естественный следующий шаг.
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, но кажется самый адекватный выход.
1. Через 4 года, когда создатель этой надстройки окэшит опцион и пойдет работать в другую компанию — пришедшему программисту достанется еще одна загадка виде «зачем это все придуманно?». Статью на хабе он гарантированно не прочитает и будет использовать фичу как Бог на душу положит
2. Надстройка гарантированно не будет применяться консистентно в течении времени жизни проекта, что добавить +1 к запутанности проекта.
3. Имутабельность списков важна, и минимизация контрактов между методами и классами тоже крайне важна. Но часто компактный и простой код, который можно легко изменить значительно важнее. Т.е. если можно сделать код в 3 раза меньше за счет использования bare Java + одной/двух абсолютно стандартных библиотек, то я предпочту меньше кода.
UPD В текущем проекте вижу библиотечный класс FastByteArray2 созданный в 2004 году — все никак не соберусь с духом заглянуть, что внутри
Затронута очень важная тема. Она касается не только коллекций, а любых сложных объектов. Вроде, сам объект сделать иммутабельным легко, но как только среди его полей появляется коллекция или другая сложная структура, то возникает вопрос, что делать с ее иммутабельностью.
На мой взгляд, в статье также не упомянут важный момент, что иммутабельные объекты не всегда могут быть иммутабельными сразу, ведь их же еще надо как-то собрать, и в процессе сборки они будут и должны быть изменяемыми.
Например, как сделать иммутабельным XML-документ?
var xml = xmlParser.parse(input);
var immutableXml = something(xml); // ??? где-нибудь такое есть?
Неизменяемых коллекций в Java не будет – ни сейчас, ни когда-либо