Ещё раз об ImmutableList в Java

    В своей предыдущей статье "Рукоблудие вокруг ImmutableList в Java" я предложил вариант решения поднятой в статье "Неизменяемых коллекций в Java не будет – ни сейчас, ни когда-либо" проблемы отсутствия в Java неизменяемых списков.


    Решение тогда было проработано только на уровне «есть такая идея», а реализация в коде была кривовата, поэтому и воспринято всё было несколько скептически. В данной статье предлагаю доработанный вариант решения. Логика использования и API доведены до приемлемого уровня. Реализация в коде – до уровня бета-версии.


    Постановка задачи


    Будем использовать определения из исходной статьи. В частности, это означает, что ImmutableList представляет собой неизменяемый список ссылок на какие-то объекты. Если эти объекты окажутся не immutable, то и список тоже не будет являться immutable объектом, несмотря на название. На практике это вряд ли кому-то помешает, но во избежание неоправданных ожиданий упомянуть надо.


    Также понятно, что неизменяемость списка может быть «хакнута» посредством reflections, или создания своих классов в том же package с последующим залезанием в protected поля списка, или ещё чего-то подобного.


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


    Будем делать библиотеку, которая может успешно сосуществовать с имеющимися в Java коллекциями.


    Основные идеи библиотеки:


    • Есть интерфейсы ImmutableList и MutableList. Приведением типов получить один из другого невозможно.
    • В своём проекте, который мы хотим улучшить с использованием библиотеки, все List-ы заменяем на один из этих двух интерфейсов. Если в какой-то момент без List-а обойтись не удаётся, то при первой же возможности преобразуем List из / в один из двух интерфейсов. То же относится к моментам получения / передачи данных в сторонние использующие List библиотеки.
    • Взаимные преобразования между ImmutableList, MutableList, List должны выполняться как можно более быстро (то есть, без копирования списков, если это возможно). Без «дешёвых» преобразований туда-обратно вся затея начинает выглядеть сомнительно.

    Следует отметить, что рассматриваются только List-ы, поскольку на данный момент в библиотеке реализованы только они. Но ничто не мешает дополнить библиотеку Set-ами и Map-ами.


    API


    ImmutableList


    ImmutableList является наследником ReadOnlyList (который, как и в предыдущей статье, представляет собой скопированный интерфейс List, из которого выкинуты все изменяющие методы). Добавлены методы:


    List<E> toList();
    MutableList<E> mutable();
    boolean contentEquals(Iterable<? extends E> iterable);

    Метод toList обеспечивает возможность передачи ImmutableList в куски кода, ожидающие List. Возвращается обёртка, в которой все изменяющие методы возвращают UnsupportedOperationException, а остальные методы переадресуются к исходному ImmutableList.


    Метод mutable преобразует ImmutableList в MutableList. Возвращается обёртка, в которой все методы переадресуются к исходному ImmutableList до момента первого изменения. Перед изменением обёртка отвязывается от исходного ImmutableList, копируя его содержимое во внутренний ArrayList, к которому далее и переадресуются все операции.


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


    Отметим, что у нашей реализации ReadOnlyList методы iterator и listIterator возвращают стандартные java.util.Iterator / java.util.ListIterator. Эти итераторы содержат изменяющие методы, которые придётся глушить выдачей UnsupportedOperationException. Красивее было бы сделать свои ReadOnlyIterator, но в этом случае мы не смогли бы написать for (Object item : immutableList), что сразу испортило бы всё удовольствие от использования библиотеки.


    MutableList


    MutableList является наследником обычного List. Добавлены методы:


    ImmutableList<E> snapshot();
    void releaseSnapshot();
    boolean contentEquals(Iterable<? extends E> iterable);

    Метод snapshot предназначен для получения «снимка» текущего состояния MutableList в виде ImmutableList. «Снимок» сохраняется внутри MutableList, и если на момент следующего вызова метода состояние не изменилось, возвращается тот же экземпляр ImmutableList. Сохранённый внутри «снимок» сбрасывается при первом вызове любого изменяющего метода, либо при вызове releaseSnapshot. Метод releaseSnapshot может использоваться для экономии памяти, если есть уверенность, что «снимок» больше никому не понадобится, но изменяющие методы будут вызваны ещё не скоро.


    Mutabor


    Класс Mutabor предоставляет набор статических методов, являющихся «точками входа» в библиотеку.


    Да, проект теперь называется «mutabor» (оно и созвучно с «mutable», и в переводе означает «я превращусь», что неплохо согласуется с идеей быстрых «превращений» одних типов коллекций в другие).


    public static <E> ImmutableList<E> copyToImmutableList(E[] original);
    public static <E> ImmutableList<E> copyToImmutableList(Collection<? extends E> original);
    public static <E> ImmutableList<E> convertToImmutableList(Collection<? extends E> original);
    public static <E> MutableList<E> copyToMutableList(Collection<? extends E> original);
    public static <E> MutableList<E> convertToMutableList(List<E> original);

    Методы copyTo* предназначены для создания соответствующих коллекций путём копирования предоставленных данных. Методы convertTo* предусматривают быстрое преобразование переданной коллекции в нужный тип, а если быстро преобразовать не удалось, то выполняют медленное копирование. Если быстрое преобразование прошло успешно, то исходная коллекция очищается, и предполагается, что в дальнейшем она не будет использоваться (хотя и может, но в этом вряд ли есть смысл).


    Вызовы конструкторов объектов-реализаций ImmutableList / MutableList спрятаны. Предполагается, что пользователь имеет дело только с интерфейсами, сам такие объекты не создаёт, а для преобразования коллекций использует описанные выше методы.


    Детали реализации


    ImmutableListImpl


    Инкапсулирует массив объектов. Реализация примерно соответствует реализации ArrayList, из которой выкинуты все изменяющие методы и проверки на concurrent modification.


    Реализация методов toList и contentEquals также достаточно тривиальна. Метод toList возвращает обёртку, перенаправляющую вызовы к данному ImmutableList, медленного копирования данных не происходит.


    Метод mutable возвращает MutableListImpl, созданный на базе данного ImmutableList. Копирования данных не происходит до тех пор, пока у полученного MutableList не будет вызван какой-либо изменяющий метод.


    MutableListImpl


    Инкапсулирует ссылки на ImmutableList и List. При создании объекта заполняется всегда только одна из этих двух ссылок, другая остаётся null.


    protected ImmutableList<E> immutable;
    protected List<E> list;

    Неизменяющие методы перенаправляют вызовы к ImmutableList, если он не null, и к List в противном случае.


    Изменяющие методы перенаправляют вызовы к List, предварительно выполнив инициализацию:


    protected void beforeChange() {
        if (list == null) {
            list = new ArrayList<>(immutable.toList());
        }
        immutable = null;
    }

    Метод snapshot выглядит так:


    public ImmutableList<E> snapshot() {
        if (immutable != null) {
            return immutable;
        }
    
        immutable = InternalUtils.convertToImmutableList(list);
        if (immutable != null) { //удалось выполнить быстрое преобразование
            //Преобразование очистило исходный список, обнуляем ссылку.
            //Список потом будет пересоздан копированием immutable в случае вызова изменяющего метода.
            list = null;
            return immutable;
        }
    
        immutable = InternalUtils.copyToImmutableList(list);
        return immutable; 
    }

    Реализация методов releaseSnapshot и contentEquals тривиальна.


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


    Быстрое преобразование списков


    Быстрые преобразования возможны для классов ArrayList или Arrays$ArrayList (результат метода Arrays.asList()). На практике в подавляющем большинстве случаев попадаются именно эти классы.


    Внутри данные классы содержат массив элементов. Суть быстрого преобразования состоит в получении ссылки на этот массив через reflections (это private поле) и замене её ссылкой на пустой массив. Это гарантирует, что единственная ссылка на массив останется у нашего объекта, и массив останется неизменным.


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


    Проблемы с equals / hashCode


    В коллекциях Java используется очень странный подход к реализации методов equals и hashCode.


    Сравнение осуществляется по содержимому, что вроде бы и логично, но при этом не учитывается класс самого списка. Поэтому, например, ArrayList и LinkedList с одинаковым содержимым будут equals.


    Вот реализация equals / hashCode из AbstractList (от которого ArrayList унаследован)
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof List))
            return false;
    
        ListIterator<E> e1 = listIterator();
        ListIterator e2 = ((List) o).listIterator();
        while (e1.hasNext() && e2.hasNext()) {
            E o1 = e1.next();
            Object o2 = e2.next();
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        return !(e1.hasNext() || e2.hasNext());
    }
    
    public int hashCode() {
        int hashCode = 1;
        for (E e : this)
            hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
        return hashCode;
    }

    Таким образом, теперь абсолютно все реализации List обязаны иметь аналогичную реализацию equals (и, как следствие, hashCode). В противном случае можно получить ситуации, когда a.equals(b) && !b.equals(a), что нехорошо. Аналогичная ситуация и с Set-ами и Map-ами.


    В приложении к библиотеке это означает, что реализация equals и hashCode для MutableList предопределена, и в такой реализации ImmutableList и MutableList с одинаковым содержимым не могут быть equals (поскольку ImmutableList не является List). Поэтому для сравнения содержимого были добавлены методы contentEquals.


    Реализация методов equals и hashCode для ImmutableList сделана полностью аналогичной варианту из AbstractList, но с заменой List на ReadOnlyList.


    Итого


    Исходники библиотеки и тесты выложены по ссылке в виде maven-овского проекта.


    На случай, если кто-то захочет использовать библиотеку, завёл группу в контактике для «обратной связи».


    Использование библиотеки довольно очевидно, вот короткий пример:


    private boolean myBusinessProcess() {
        List<Entity> tempFromDb = queryEntitiesFromDatabase("SELECT * FROM my_table");
        ImmutableList<Entity> fromDb = Mutabor.convertToImmutableList(tempFromDb);
        if (fromDb.isEmpty() || !someChecksPassed(fromDb)) { return false; }
        //...
        MutableList<Entity> list = fromDb.mutable(); //time to change
        list.remove(1);
        ImmutableList<Entity> processed = list.snapshot(); //time to change ended
        //...
        if (!callSideLibraryExpectsListParameter(processed.toList())) { return false; }
        for (Entity entity : processed) { outputToUI(entity); }
        return true;
    }

    Всем удачи! Шлите багрепорты!

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

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

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

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

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

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

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

              0
              Тем не менее, если бы был тип аргумента OrderedList (не просто Iterable) или какой то подобный (не очень знаком с Java), то ИМХО было бы приятнее.
              Но в целом, да, согласен, посмотрел ещё документацию, так очевидно
            0
            Меня восхищает Ваша напористость и желание сделать 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;
            Но это совсем другая история, не находите?

              –1

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

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

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

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

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


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

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

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

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

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

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

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

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

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

                  (комментарий был удалён)

                    +3

                    Сейчас уже интереснее, какое-то 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 именно для этого и существует.

                      0

                      Ещё текущая реализация жестоко ломает 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! В рот мне ноги!
                        0

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

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

                      Самое читаемое