List.of() и все, все, все…

habr.png

Здравствуйте, хаброжители. Наконец дошли руки написать что-то на хабр. Первая статья была немного скучной и узкоспециализированной. Поэтому я пишу в песочницу во второй раз. (UPD, но почему то попал не в песочницу оО)

На этот раз речь пойдет о нововведениях Java. А именно про ImmutableCollections. Вы наверное где-то уже использовали List.of (). Скорее всего в тестах, ибо какой-то практической нужды в них нет. Но даже в тестах можно наткнуться на банальные подводные камни. Банальны они по причине того, что разок прочитав их код, сразу все становится на свои места, но остается очень и очень много вопросов, почему сделанно именно так, а не по другому.

Начнем пожалуй с интерфейса List, в котором есть статические функции of.

Целых ОДИННАДЦАТЬ!!!
List List.of(E e1);  

List List.of(E e1, E e2);  

List List.of(E e1, E e2, E e3);  

List List.of(E e1, E e2, E e3, E e4);  

List List.of(E e1, E e2, E e3, E e4, E e5);  

List List.of(E e1, E e2, E e3, E e4, E e5, E e6);  

List List.of(E e1, E e2, E e3, E e4, E e5, E e6, E e7);  

List List.of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8);  

List List.of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9);  

List List.of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10);  

List List.of(E... elements)  


Зачем же в джаве столько методов??? Долго я задавался этим вопросом, но никак не доходили руки порыться в source коде и погуглить. Сегодня таки я нашел ответ, который меня абсолютно неудовлетворил.

The fixed argument overloaded methods are provided to save the overhead of array-allocation, initialization and garbage collection in case of vararg calls.


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

Если мы посмотрим код этих методов, то увидем следующее:

static  List of() {  
        return ImmutableCollections.List0.instance();  
    }  
static  List of(E e1) {  
        return new ImmutableCollections.List1<>(e1);  
    }  

static  List of(E e1, E e2) {  
        return new ImmutableCollections.List2<>(e1, e2); 
    }  

static  List of(E e1, E e2, E e3) {  
        return new ImmutableCollections.ListN<>(e1, e2, e3);  
    }  

static  List of(E e1, E e2, E e3, E e4) {  
        return new ImmutableCollections.ListN<>(e1, e2, e3, e4);  
    }  

/* ... Все остальные такие же и ... */  

static  List of(E... elements) {  

        switch (elements.length) { // implicit null check of elements  
            case 0:  
                return ImmutableCollections.List0.instance();  
            case 1:  
                return new ImmutableCollections.List1<>(elements[0]);  
            case 2:  
                return new ImmutableCollections.List2<>(elements[0], elements[1]);  
            default:  
                return new ImmutableCollections.ListN<>(elements);  
        }  
    }  


Первые 3 метода возвращают нам странные объекты классов List0, List1, List2. Все остальные вернут ListN. И последний метод, в котором используются varargs может вернуть один из перечисленных списков. Начиная от 3 элементов и до 10 мы действительно отправляем в метод аргументы, и ни в какой массив эти данные не обернутся.

Казалось бы, как же все хорошо, но давайте копать еще дальше. Посмотрим реализацию конструктора ListN<>

ListN(E... input) {
            @SuppressWarnings("unchecked")
            E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input
            for (int i = 0; i < input.length; i++) {
                tmp[i] = Objects.requireNonNull(input[i]);
            }
            this.elements = tmp;
        }


Как можно заметить, тут уже присутствует синтаксис varargs. А это значит, что даже если во время первого вызова оборачивания в массив избежать удалось, в случае вызова конструктора это все равно произошло. И к чему нужна была такая вот реализация? Этот вопрос все еще для меня открыт. Буду рад, если кто в комментариях расскажет.

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

Во главе всех Immutable list-ов стоит:

abstract static class AbstractImmutableList extends AbstractList
                                                implements RandomAccess, Serializable


Все методы этой абстрактой коллекции кидают UnsupportedOperationException. Не все абстрактные методы из AbstractList заоверрайдили в этом классе. Поэтому прилагаю код:

        @Override public boolean add(E e) { throw uoe(); }
        @Override public boolean addAll(Collection c) { throw uoe(); }
        @Override public boolean addAll(int index, Collection c) { throw uoe(); }
        @Override public void    clear() { throw uoe(); }
        @Override public boolean remove(Object o) { throw uoe(); }
        @Override public boolean removeAll(Collection c) { throw uoe(); }
        @Override public boolean removeIf(Predicate filter) { throw uoe(); }
        @Override public void    replaceAll(UnaryOperator operator) { throw uoe(); }
        @Override public boolean retainAll(Collection c) { throw uoe(); }
        @Override public void    sort(Comparator c) { throw uoe(); }


То есть, к примеру, метод containsAll будет иметь ту же реализацию, что и все остальные коллекции, которые мы успешно использовали, и то не всегда. Но сейчас не об этом.
Классы List0, List1, List2 и ListN наследуются от класса AbstractImmutableList. Каждый класс реализует часть методов.

Возьмем к примеру класс List0. Метод contains может выкинуть NullPointerException.

@Override
        public boolean contains(Object o) {
            Objects.requireNonNull(o);
            return false;
        }


Вот это очень неожиданно. Почему нельзя было просто всегда возвращать false? Зачем тут проверка на null. Это мне остается непонятным.

Поведение метода containsAll такое же, как и в обычных списках.

        public boolean containsAll(Collection o) {
            return o.isEmpty(); // implicit nullcheck of o
        }


Правда NPE выскочит из-за вызова метода isEmpty (), а не цикла for each как в обычных списках.
В source коде я заметил один комментарий, который поднял настроение и напомнил мне, сколь машина (речь больше о компиляторе) глупее человека.

@Override
        public E get(int index) {
            Objects.checkIndex(index, 0); // always throws IndexOutOfBoundsException
            return null;                  // but the compiler doesn't know this
        }


Поехали далее к List1. Тут вопросов больше. Начнем с конструткора.

List1(E e0) {
            this.e0 = Objects.requireNonNull(e0);
        }


Почему у меня в списке не может храниться null? В чем тут логика? Поехали далее. Метод contains все так же выкидывает NPE.

@Override
        public boolean contains(Object o) {
            return o.equals(e0); // implicit nullcheck of o
        }


Хотя тут уже логичнее. Если конструктор не дает создание списков с null, то это ожидаемо. Но что мешало написать:

return o != null && o.equals(e0);


или куда красивее:

return e0.equals(o);


Опять же опираясь на то, что e0 не может быть null. Реализация метода containsAll лежит в классе AbstractCollection:

public boolean containsAll(Collection c) {
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
    }


Если коллекция null, то мы получим такой же NPE, как и в случае с обычными коллекциями. Но так же мы получим NPE, если в коллекции из параметров есть null, так как присутствует зависимость от метода contains, который и будет давать нам этот NPE.

При чем NPE здесь достаточно опасный.

List list = new ArrayList<>();
list.add("FOO");
list.add(null);
List immutableList = List.of("foo", "bar");
immutableList.containsAll(list);


В этом случае мы не словим NPE, так как наш список не содержит строки «FOO». Мы сразу получим в ответ false. Если же в нашем ArrayList первый элемент был бы «foo», то мы тут же словили бы NPE. Поэтому, будьте крайне осторожны в таких ситуациях.

List2 и ListN грешны тем же.

Обобщая написанное, у меня все еще остается несколько вопросов. Почему данные коллекции не ведут себя так же, как и обычные ArrayList, LinkedList? Почему коллекции не могут содержать null. Зачем было создавать столько методов? Неужели этот код накидан на скорую руку и никто не хочет им заниматься? Но, так как ответы на эти вопросы я дать не могу, остается использовать, что есть, зная о подводных камнях, которые присутствуют в этих новых удобных фичах.

П.С. Предполагаю, что Set и Map тоже имеют свои схожие подводные камни. Но до них я дотянусь как-нибудь потом, когда появится еще несколько минут свободного времени.

© Habrahabr.ru