Вам не кажется, что с таким подходом нарушается принцип подстановки Лисков?
Нет. Например, в контракте метода List.add явно прописано: throws UnsupportedOperationException if the add operation is not supported by this list. То есть, вызывающая сторона должна быть к такому готова. На практике в «родных» коллекциях такое исключение можно получить, например, от списка, возвращённого Collections.unmodifiableList.
Может стоит явно указать ограничение это в интерфейсе?
Хотелось бы, но не знаю, как. Например, TreeSet.iterator вернёт итератор с элементами в порядке возрастания. И как (кроме как захардкодить) мне его отличить от HashSet?
Если он будет работать за O(log N), то не проблема (хотя всё равно менее приятно, чем О(1) в обычном списке). Вот только и написать такое очень не просто, и реализация получится, видимо, на базе деревьев, а это означает, что памяти на «накладные расходы» уйдёт в разы больше по сравнению с простым массивом ссылок. А пользы — пшик.
Iterable не дает гарантии ReadOnly, т.к. в Iterator-е есть метод remove()
Ну так Iterator-то мы сами пишем… Метод remove() кидает UnsupportedOperationException. Безусловно, с ReadOnlyIterator было бы красивее, но приходится подстраиваться под существующую Java — Iterable нужен, чтобы можно было написать for (Item item : ImmutableList<Item>)
сделать новый java.collections и добавить конвертеров
Это непрактично, т.к. все библиотеки используют «старые» коллекции, а конвертация — тяжёлая операция. Основная идея того, что я описал в статье — как раз «дешёвая» конвертация туда-обратно.
Это «создать копию списка с добавленным элементом»? Нет-нет, вот такого точно не надо. А то потом это обязательно будет по запарке / рукозадости запихнуто в какой-нибудь гигантский элементодобавительный цикл, и наступит всеобщее счастье. Считаю, что подобные сомнительные моменты должны более явно в API выражаться: нужны изменения — сконвертировали ImmutableList в MutableList, внесли изменения, потом сконвертировали обратно (если в итоге ImmutableList нужен).
Да, вы правы. От конструктора такого не очень ожидаешь. Поэтому в следующей версии будет специальный метод. (Зуд не прошёл, поэтому следующая версия, похоже, всё-таки будет)
Так про то и речь, рефлекшены — это костыль для крайних случаев, и программисты (обычно) это понимают. А «штатными» средствами, считаю, не должно быть даже потенциальной возможности всё испортить. То есть, приведения типов MutableList <-> ImmutableList просто скомпилироваться не должны.
2) Ваша реализация похожа на грабительство — Вы отбираете массив у вызывающего объекта. Это антипаттерн.
Обоснуете, почему «анти»? Например, из ORM вы получили ArrayList<MyEntity>, понимаете, что меняться он не будет, и хотите дальше в своём коде использовать его как ImmutableList<MyEntity>. Преобразовали, старый список ушёл, новый появился. Чем плохо?
3) На самом деле, ImmutableList-ы в большинстве библиотек построены на основе сбалансированных деревьев, поэтому, изменение листа проходит за O(log(N)).
Вот это вообще не понял. Какие изменения ImmutableList-а?
4) Вы придумали кривой велосипед, который никто в здравом уме не примет за стандарт.
Кривой он только тем, что использует рефлекшены (если вдруг поменяют название поля ArrayList#elementData — поломается). Естественно, если бы это делали авторы Java, можно было бы без рефлекшенов обойтись.
Да, получается не полностью иммутабельный объект, а иммутабельный список потенциально мутабельных объектов (как в определениях исходной статьи).
Получить «иммутабельный список иммутабельных объектов» — задача интересная, но вряд ли на Java решаемая. Хотя если в списке лежат только голые данные в виде POJO, то можно было бы сделать для них иммутабельные копии через рефлекшены. Но это очень долго и уже поэтому не нужно.
В Java для такого есть готовый Collections.unmodifiableList. Просто он для такой ситуации ничем не лучше, чем по-настоящему immutable список (если последний достаточно быстрый). Зато с immutable взаимные ожидания методов более явно задекларированы. Хотя возможны и ситуации, когда immutable не подходит — как раз когда изменяемость нужна — вызываемый метод не может менять список, но знает, что его может менять вызывающий метод, и умеет это обрабатывать. Но это отдельные случаи, редкие и неприятные как раз необходимостью этой особой обработки.
Ну это понятно, что по возможности стоит избегать. Но согласитесь, что привычный к написанию equals программист обычно воспринимает каст гораздо менее болезненно, чем рефлекшены.
Бывают неприятные случаи, когда приходится хакать. Для этих случаев есть рефлекшены. Но к ним обычно и отношение соответствующее: «это ружьё рано или поздно стрельнёт в ногу». А приведение типов — вполне штатная операция, выполняемая на каждом углу, её легко можно написать, не особо задумываясь или «временно для тестов, потом поправлю». И когда код читаешь, оно не особо в глаза бросается. А если уж сначала UnmodifiableList станет, например, переменной типа Object (всякое в жизни бывает), то в другом месте кастинг в List будет выглядеть вообще абсолютно невинно.
Если, как предложено в статье, List является наследником UnmodifiableList, то получим ту же проблему, что в начале статьи описана: в API на входе UnmodifiableList, вызывают его с экземпляром List, тогда реализация API внутри может спокойно сделать приведение типов к List и менять, что захочет. Альтернативы — либо копирование, либо обёртка (как сейчас и сделано в Collections.unmodifiableList()). В обоих случаях оверхеда не избежать. Обёртка на первый взгляд кажется меньшим оверхедом, но если вспомнить про сборку мусора, то разница может оказаться совсем небольшой.
Сомневаюсь в полезности UnmodifiableList в случае наличия ImmutableList. В вашем примере: public UnmodifiableList<Agent> teamRoster() — вот получили список, начали его на экран выводить, а он в это время поменялся из другой нити. Единственный вариант придумался, когда нужен именно UnmodifiableList без «стрельбы в ногу» — метод, циклически опрашивающий объекты на предмет какого-то события (пропустили один объект — ничего, на следующем цикле опросим; опросили один объект два раза за цикл — тоже ничего).
А вот что мешает изготовить свой ImmutableList и использовать в своих проектах? Не обязательно же всю экосистему менять. Получили от библиотеки List — скопировали сразу же содержимое в свой ImmutableList и дальше его гоняете. Лишнее копирование неприятно, но без него не обойтись.
Да, абсолютно так — проще и дешевле. Потому я и не особо верю в воспитание критичного мышления. В теории это было бы здорово, а на практике — гос властям это не нужно (пропаганда выгоднее), людям тоже зачастую проще не думать лишнего, а принять чью-то точку зрения. В итоге из хорошей идеи получится либо ничего, либо воспитание чего-то совсем другого.
List.add
явно прописано:throws UnsupportedOperationException if the add operation is not supported by this list
. То есть, вызывающая сторона должна быть к такому готова. На практике в «родных» коллекциях такое исключение можно получить, например, от списка, возвращённогоCollections.unmodifiableList
.Хотелось бы, но не знаю, как. Например,
TreeSet.iterator
вернёт итератор с элементами в порядке возрастания. И как (кроме как захардкодить) мне его отличить отHashSet
?for (Item item : ImmutableList<Item>)
Это непрактично, т.к. все библиотеки используют «старые» коллекции, а конвертация — тяжёлая операция. Основная идея того, что я описал в статье — как раз «дешёвая» конвертация туда-обратно.
(Хотя после этого пришла и мрачная мысль: в ReactOS «еще результативнее» может значить, что раньше загрузка падала на 70%, а теперь на 90%)
ArrayList<MyEntity>
, понимаете, что меняться он не будет, и хотите дальше в своём коде использовать его какImmutableList<MyEntity>
. Преобразовали, старый список ушёл, новый появился. Чем плохо?Вот это вообще не понял. Какие изменения ImmutableList-а?
Кривой он только тем, что использует рефлекшены (если вдруг поменяют название поля
ArrayList#elementData
— поломается). Естественно, если бы это делали авторы Java, можно было бы без рефлекшенов обойтись.Получить «иммутабельный список иммутабельных объектов» — задача интересная, но вряд ли на Java решаемая. Хотя если в списке лежат только голые данные в виде POJO, то можно было бы сделать для них иммутабельные копии через рефлекшены. Но это очень долго и уже поэтому не нужно.
Collections.unmodifiableList
. Просто он для такой ситуации ничем не лучше, чем по-настоящему immutable список (если последний достаточно быстрый). Зато с immutable взаимные ожидания методов более явно задекларированы. Хотя возможны и ситуации, когда immutable не подходит — как раз когда изменяемость нужна — вызываемый метод не может менять список, но знает, что его может менять вызывающий метод, и умеет это обрабатывать. Но это отдельные случаи, редкие и неприятные как раз необходимостью этой особой обработки.List
является наследникомUnmodifiableList
, то получим ту же проблему, что в начале статьи описана: в API на входеUnmodifiableList
, вызывают его с экземпляромList
, тогда реализация API внутри может спокойно сделать приведение типов кList
и менять, что захочет. Альтернативы — либо копирование, либо обёртка (как сейчас и сделано вCollections.unmodifiableList()
). В обоих случаях оверхеда не избежать. Обёртка на первый взгляд кажется меньшим оверхедом, но если вспомнить про сборку мусора, то разница может оказаться совсем небольшой.public UnmodifiableList<Agent> teamRoster()
— вот получили список, начали его на экран выводить, а он в это время поменялся из другой нити. Единственный вариант придумался, когда нужен именно UnmodifiableList без «стрельбы в ногу» — метод, циклически опрашивающий объекты на предмет какого-то события (пропустили один объект — ничего, на следующем цикле опросим; опросили один объект два раза за цикл — тоже ничего).А вот что мешает изготовить свой ImmutableList и использовать в своих проектах? Не обязательно же всю экосистему менять. Получили от библиотеки List — скопировали сразу же содержимое в свой ImmutableList и дальше его гоняете. Лишнее копирование неприятно, но без него не обойтись.
Современная двухсекундная — это про что речь?