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

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

Я правильно понял что при подсчёте hashCode при каждом вызове просматриваются все элементы списка, в том числе и для ImmutableList?
А для equals попарно просматриваются элементы списков до первого несовпадения?
Ну да. Это штатное поведение списков в Java, а для ImmutableList сделано по аналогии.
Метод toList обеспечивает возможность передачи ImmutableList в куски кода, ожидающие List. Возвращается обёртка, в которой все изменяющие методы возвращают UnsupportedOperationException, а остальные методы переадресуются к исходному ImmutableList.

Вам не кажется, что с таким подходом нарушается принцип подстановки Лисков?

Метод contentEquals предназначен для сравнения содержимого списка с содержимым произвольного переданного Iterable (разумеется, осмысленной эта операция является только для тех реализаций Iterable, у которых есть какой-то внятный порядок элементов).

Может стоит явно указать ограничение это в интерфейсе?
Вам не кажется, что с таким подходом нарушается принцип подстановки Лисков?
Нет. Например, в контракте метода List.add явно прописано: throws UnsupportedOperationException if the add operation is not supported by this list. То есть, вызывающая сторона должна быть к такому готова. На практике в «родных» коллекциях такое исключение можно получить, например, от списка, возвращённого Collections.unmodifiableList.
Может стоит явно указать ограничение это в интерфейсе?
Хотелось бы, но не знаю, как. Например, TreeSet.iterator вернёт итератор с элементами в порядке возрастания. И как (кроме как захардкодить) мне его отличить от HashSet?
Нет. Например, в контракте метода List.add явно прописано: throws UnsupportedOperationException if the add operation is not supported by this list.

Спасибо за пояснение.
Но по мне, все ещё выглядит странно. Если Collections.unmodifiableList не реализует List.add, то и реализовывать его не стоит. Разделили бы на два интерфейса.
Это не к Вам, конечно, а просто недоумение)
Может стоит явно указать ограничение это в интерфейсе?

Тот случай, когда это ограничение очевидно.

Тем не менее, если бы был тип аргумента OrderedList (не просто Iterable) или какой то подобный (не очень знаком с Java), то ИМХО было бы приятнее.
Но в целом, да, согласен, посмотрел ещё документацию, так очевидно
Меня восхищает Ваша напористость и желание сделать SDK лучше.
Сразу скажу, что я согласен с автором поста Неизменяемых коллекций в Java не будет – ни сейчас, ни когда-либо
И это согласие есть результат статьи, которую я практически написал, но в последний момент отказался публиковать — так как изменил свое мнение на мнение автора :)
Вот совсем кратко мои рассуждения на тему списков:

Чтобы ответить на этот вопрос, сначала нужно поискать уже существующие решения в самом SDK, а во-вторых, посмотреть на потенциальное решение с практической точки зрения. Начнем с поиска.
Классическим представителем неизменяемых объектов в SDK являются экземпляры класса java.lang.String. Строки спроектированы таким образом, что поменять их через публичное API — невозможно. Строки имеют отношение к поиску истины и потому, что их можно рассматривать через призму коллекций. Фактически, строка — это набор символов. Но сейчас вернемся к самому типу String. Важно понять, что в SDK нет MutableString. Или перефразировав, прийти к тому, что нет такой полной иерархии как ImmutableString, UnmodifiableString и MutableString. Плохо это или хорошо — каждый решает сам.
Что касается иерархии, то я немного слукавил — иерархия есть, только не для типа String, как чего-то целого и завершенного, а для набора символов. Это всем известные StringBuilder и StringBuffer, которые как и String растут от CharSequence. Фактически, у нас есть 2 ветки: изменяемые и неизменяемые последовательности символов. Причем во главу угла поставлен контракт на чтение данных, так как CharSequence содержит только методы получения данных.
После ознакомления с готовым решением по строкам в SDK можно сделать несколько важных выводов про mutable и immutable:

1) Каждая реализация сохраняет свое свойство навсегда — нет перехода от immutable к mutable или обратно внутри одной реализации.
2) В любой части программы, работая с реализацией, мы можем точно сказать о ее свойствах — изменяемая (StringBuilder/StringBuffer) или неизменяемая (String).
3) В любой части программы, работая с абстракцией (CharSequence), мы не можем сказать, какими свойствами данное представление обладает и какие гарантии может обеспечить (в общем случае).
4) Логичным и подтвержденным реализацией подходом является переход от mutable к immutable, т.е. изменяемый набор порождает неизменяемый, но не наоборот.

Выводы как постулаты мы будем использовать для реализации неизменяемых списков. Но сначала, я хотел бы проанализировать сами выводы и понять их сильные и слабые стороны. Мой любимый — это пункт 3. На мой взгляд, для проектирования безопасного api — это огромный недостаток. Когда мы спускаем в engine некоторую абстракцию, вполне логичным, для меня, является то, что этот engine имеет право потребовать определенные гарантии надежности от источника, взамен, обещает сделать свою работу должным образом. Такие отношения можно формализовать с помощью простого метода, спросив с помощью него, какие гарантии несет реализация. В плоскости рассматриваемой проблемы им может стать контрактный метод isImmutable;
    boolean isImmutable();

Если мы используем фундаментальные принципы проектирования, то делать такой метод — неправильно. Но если мы ищем больше практичное решение, нежели фундаментальное — то допустимо.
Так как пункт 1 гласит, что свойство immutable/mutable сохраняется навсегда, то вмести с ним сохраняется и гарантия неизменяемости/изменяемости тоже — навсегда.
Рассматривая пункт 3, мы столкнулись с первой дилеммой — фундаментальность или практичность?
Пункт 2, можно интерпретировать и по-другому, что не должна одна реализация рости от второй. Т.е. ссылочное присвоение в обе стороны запрещены (ошибка компиляции).
Пункт 4, опять таки заставляет нас разобраться с дилеммой: фундаментальность или практичность? Потому что требует, наличие метода toImmutable в общем контракте. Можно читать toImmutable здесь как метод toString в классе CharSequence, с одной оговоркой.
    List<T> toImmutable();

Я утверждаю, что пункт 4 больше рожден из практической плоскости, нежели фундаментальной. И это важно. В противном случае, неизменяемая реализация должна порождать изменяемую, но на примере String, мы видим, что такого нет.
Есть еще одна особенность в типах String, StringBuilder и StringBuffer, если рассматривать их как набор символов, про которую я нарочно умолчал. Мы всегда работаем с реализацией, что сильно упрощает контекст восприятия.
Это недопустимо для списков в общем случае.
Если подытожить выше изложенную философию мысли, для решения проблемы с неизменяемыми коллекциями нам требуются в арсенале 2 метода isImmutable и toImmutable. Разумеется, исходим из того, что по факту сейчас есть в SDK.
interface Collection<E> extends Iterable<E> {
    // ...
    boolean isImmutable();
    Collection<E> toImmutable();
    // ...
}

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

И так далее, пока я не пришел к выводу, что новых коллекций не появится.

Если попытаться выдать желаемое за действительное — то в SDK все же неизменяемые коллекции могут быть и мне точно известно время — когда в java появится система типов, подобная TypeScript. Тогда различия между 2мя коллекциями будет в наличие значение у поля mutable: или true или false;
Но это совсем другая история, не находите?

Простите, но откуда взялся метод isImmutable, с которым вы спорите?

Во-первых, с методом я не спорю (только с людьми)

А во-вторых:
>> 3) В любой части программы, работая с абстракцией (CharSequence), мы не можем сказать, какими свойствами данное представление обладает и какие гарантии может обеспечить (в общем случае)

Метод isImmutable — это следствие моих рассуждений по части пункта 3 и в контексте статьи, которую в качестве исходного материала я указал в начале.

Вот именно, мы это не можем сказать. Так зачем тогда нужен метод?


Если кому-то нужны гарантии — пусть явно попросит неизменяемый вариант коллекции.

Вы чертовски проницательны: Если кому-то нужны гарантии — пусть явно попросит неизменяемый вариант коллекции.

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

Так в чём проблема-то?

Добро пожаловать в наш клуб.
1) В java SDK нет контракта на неизменяемую (немодифицируемую) коллекцию, пожалуй основная проблема.
2) Если такой контракт добавлять, то неизбежно придется ответить на вопросы:
2.1) Какой интерфейс должен быть у неизменяемой коллекции?
2.2) Должен ли этот интерфейс/контракт наследоваться от Collection/List?
2.3) Должен ли Collection/List наследоваться от неизменяемой коллекции?
2.4) Можно ли ввести понятие неизменяемой коллекции, не сломав обратную совместимость с ранними версиями?
2.5) Должна ли неизменяемая коллекция порождать новую в методах add/remove и т.д. или бросать исключение?
Я осветил ключевые вопросы и проблемы. У каждого ответа есть свои последствия, которые могут влиять на всю эко систему SDK.

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

Я думаю, самое время послушать Вас: Как Вы оцениваете предложенную реализацию в данном посте? Плюсы, минусы разумеется…

Чем-то выдающимся она точно не является, но и грубых ошибок не вижу.

Я возмоно пропустил это, но почему нельзя использовать маркерные интерфейсы?
Два интерфейса с операциями (Read, Write), один маркерный (Immutable), оригинальный List (extends Read, Write), и нужный ImmutableList (extends Read, Immutable).
Основное, почему нельзя — это наличие существующих List. Если бы переделывать коллекции Java с нуля, то так можно было бы сделать. А в текущей реальности из такого подхода получится просто альтернативный Collections Framework, который не взлетит, поскольку все сторонние библиотеки под List заточены.
А, понял, задача была использовать существующую иерархию.
Если честно, далеко не все ваши мысли понял, но:
— Одной из основных идей описанной в статье реализации было именно что не «сделать SDK лучше» (это да, близко к невозможному), а реализовать компромиссный вариант между существующим SDK и «как оно могло бы быть в идеале».
— Вот вы говорите «работая с абстракцией (CharSequence), мы не можем сказать, какими свойствами данное представление обладает и какие гарантии может обеспечить». На самом деле, CharSequence описывает только read-only методы. То есть, если метод на входе принимает CharSequence, то трудно ожидать, что увидев, что ему передали StringBuilder, он сделает приведение типов и начнёт что-то менять (и даже лучше, если бы такое приведение типов не позволял сделать компилятор).
— Аналогично и для коллекций: да, вариант с флажком isImmutable возможен, но гораздо лучше, если интерфейсы для read-only и для read-write операций разные. Тогда глянув на сигнатуру метода вы сразу понимаете, будет он что-то менять, или нет.
— Хотя флажок isImmutable я бы хотел видеть, но не для коллекций, а для вообще всех поголовно объектов, и не для конкретных экземпляров, а для классов целиком.

Сейчас уже интереснее, какое-то API вырисовывается. Маленькое замечание по имени — вместо toList() лучше asList(). Предлог to традиционно сигнализирует о преобразовании, а адаптеры именуются начиная с as.


Метод convertToImmutableList — это очень грязно. Чистое и понятное API гораздо лучше сомнительного прироста производительности. Но если уж вы это делаете, позвольте дать несколько советов.


Во-первых, сейчас семантика метода совершенно плохая. Он делает что-то, если что-то сработает, в зависимости от типов коллекций. А если не сработает, то результат будет другим. Надёжная система на это никогда не пойдёт. Гораздо разумнее сделать одинаковую семантику, и хачить только производительность. Можно сделать, например, метод drainToImmutableList(Collection<T> c), который всегда перегружает данные в неизменяемый список и очищает исходный (вызывая c.clear()). Для такого метода можно сделать быстрый путь для ArrayList. Он будет работать так же, даже если рефлекшн не сработает.


Во-вторых, рекомендую заменить original instanceof ArrayList<?> на original.getClass().equals(ArrayList.class). Мало ли кто какой наследник сделал и каких гарантий он ожидает в этом наследнике.


В-третьих, вы зря инициализируете поля безусловно в статическом инициализаторе. В Java 9+ пользователь библиотеки получит в консоль предупреждение о том, что используется нелегальный рефлекшн, просто когда он воспользуется вашей библиотекой, даже если не будет использовать этот спорный метод. Внутри библиотеки вы не можете это отключить, пользователю придётся писать дурацкие ключики при запуске JVM. Не каждый будет рад. Надо сделать это лениво (например, используя Initialization on demand holder idiom), чтобы не страдали те, кто не хочет использовать сомнительные методы.


Ещё я бы не делал пакет internal, а вместо этого сделал package-private классы в том же пакете, чтобы в Java 8 оно не торчало наружу. Package-private именно для этого и существует.

Ещё текущая реализация жестоко ломает subList. Пример:


    ArrayList<String> list = new ArrayList<>();
    list.add("foo");
    list.add("bar");
    list.add("baz");
    List<String> strings = list.subList(0, 3);
    ImmutableList<String> l1 = Mutabor.convertToImmutableList(list);
    System.out.println(strings.size()); // всё ещё 3
    System.out.println(strings.get(0)); // oops: ArrayIndexOutOfBoundsException

А можно ещё веселее:


ArrayList<String> list = new ArrayList<>();
list.add("foo");
list.add("bar");
list.add("baz");
List<String> strings = list.subList(0, 3);
ImmutableList<String> l1 = Mutabor.convertToImmutableList(list);
for (int i = 0; i < 20; i++) {
    strings.add(0, "x"); // работает!
}
System.out.println(strings.get(20)); // печатает null! В рот мне ноги!

Можно попробовать установить только size, а потом вызвать легально trimToSize. Может быть существенно лучше.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории