Коллекции
Классы-обертки и коллекции
Использование оберток для примитивов может вызвать проблемы, которые сложно обнаружить:
1 | List<Integer> list = new ArrayList(); |
В примере выше, первый remove(int) будет расцениваться как метод удаления по индексу. Второй вызов remove(Integer) рассматривается как удаление объекта.
Коллекции
List- отсортированная (по очереди добавления) коллекция, позволяющая выполнять вставку дубликатов, а так же предоставляет доступ по индексу;Set- не допускает дубликатов, а так же доступ по индексу. По факту, использует Map для хранения данных;Queue- очереди, сортирующие элементы в специальном порядке. Для большинства реализаций, по умолчанию - FIFO.
Основные методы коллекций
add(E)- возвращаетtrueилиfalseв случае если реализация коллекции допустила вставку. По умолчанию, метод может вернутьfalseесли объект уже есть; при ошибке - может быть выброшено исключение. Так, например,Setвернетfalseдля дублей, аListвсегда возвращаетtrue;remove(Object)- удаляет объект из коллекции. ДляListметодremove(int)перегружает базовый метод и удаляет элемент коллекции по индексу. Метод выкинетConcurrentModificationExceptionпри выполнении в цикле for и for-each. Следует использовать либо безопасную реализациюList, либоListIterator;removeIf(Predicate<? extends E> test)- удаляет все объекты, которые соответствуют условию - методtest(E)вернет true для элемента. Ссылки с параметрами не допускаются (т.е. метод может быть только: а) статическим; б) принимать толькоE; в) возвращатьboolean;replaceAll(UnaryOperator<E>)- переписывает элементы новыми значениями:
1 | List<Integer> list = ...; |
Реализации коллекций
ArrayList- наиболее часто используемый вид списков. Основной плюс в предсказуемом доступе к элементу. Добавление, замена и удаление заметно медленнее чтения;LinkedList- реализуетListиQueue. Быстрый доступ к началу и концу списка, но для доступа к объекту в середине потребуется перебор (до момента обнаружения элемента).
Создание List через фабрики
Arrays.asList(varargs)- массив с фиксированной длинной. Конвертирует массив вList. Причем,Listполучается с фиксированной длинной - не добавить, ни убавить нельзя. Но можно использоватьset(int, E). При этом, массив иListстанут неразрывно связаны ссылками на объекты:
1 | String[] arr = {"abc", "def"}; |
List.of(varargs)- метод создает неизменный (immutable)List, который уже никак не будет связан с оригинальным массивом. Так же, в этомListнельзя не удалить ни добавить, ни изменить (!) состав списка (в т.ч. черезset(int, E));List.ofCopy(Collection)- неизменный (immutable) список, который является копией исходной коллекции.
1 | String[] array = {"a", "b", "c"}; |
В последнем вызове remove исключение не возникнет, поскольку до фактического удаления не дойдет, т.к. элемента просто нет в списке.
Создание Set через фабрики
Set.of- создает неизменную (immutable) коллекцию. Не дает вставлятьnullили дубли - будет выброшено исключениеIllegalArgumentException: duplicate element:
1 | Set.of("a", "b", "c"); |
- Set.copyOf - аналог
List.ofCopy, создаетimmutableкопию коллекции:
1 | Set.copyOf((Collection) collection); |
List
Некоторые методы:
remove(Object)
Удаляет объект, который будет такой же (метод equals вернет true). Важно внимательно смотреть за сигнатурой метода - для Object возвращает boolean.
remove(int)
Так же удаляет объект из списка по индексу, но возвращает тот объект, который был удален.
set(int, E)
Заменяет объект в позиции int на объект E. Возвращает старый объект. Если по индексу никакого объекта нет, то будет выброшена ошибка IndexOutOfBoundsException.
contains(E)
На каждый элемент проверяет equals. Может принимать null - тогда будет выполнен поиск первого null объекта.
equals(Object)
Проверяет два объекта List на полное соответствие друг другу, обращая внимание на количество, очередность и equals объектов в составе.
toArray(T[])
Принимает T[], который определяет тип результирующего массива. Может принимать массив с размером больше 0, но тогда если будет его нехватка, то будет создан новый массив и будет фактически израсходован двойной объем памяти - по-этому рекомендуется передавать с объемом 0. Создается новый массив со ссылками на объекты (следует помнить, String - immutable объект).
Примитивы в List
Поскольку в коллекциях хранение примитивов недопустимо, они принудительно оборачиваются в соответствующие обертки (int -> Integer). При подобной конвертации, Java сама выполняет метод valueOf от класса обертки. Данный метод использует кэширование, что способствует быстрой работе, а так же не дублирует объект. Размер кэша определяется в параметрах JVM, так что его лучше расширить, если ожидается частая работа с примитивами.
Классы-обертки так же имеют метод parse (parseInt, parseDouble и т.д.), которые читают строку в примитив. Если будет передан символ, не соответствующий обертке, будет ошибка. Для double и float используется “.”.
Queue
Класс LinkedList реализует List и Queue, беря плюсы от обоих интерфейсов.
При этом, ArrayDequeue превосходит его в эффективности при той же функциональности.
E element()- возвращает следующий элемент из очереди, илиExceptionесли больше нет доступных элементов;boolean offer(E)- пытается добавить элемент в конец очереди. Результат успешное (или нет) выполнение;E poll()- возвращает следующий доступный элемент из очереди и при этом удаляет его из очереди;E peek()- возвращает следующий элемент, но в отличие отpoll, не удаляет его из очереди;E remove()- аналогpoll, но не кидает исключение, если элементnull, но выкинет если очередь пустая.
Map
Map.of- неизменный (immutable) map. Имеет метод, который сам различает ключи и значения, а так же определяет к ним тип:
1 | Map<String, Integer> map1 = Map.of("A", 1, "B", 2, "C", 3); |
Map.ofEntries- неизменный (immutable)Map. ПринимаетvarargsизMap.Entry:
1 | Map.ofEntries(Map.entry("A", 1), Map.entry("B", 2), Map.entry("C", 3)); |
Основные методы Map
forEach(BiConsumer<K, V>)- листает каждый элемент вMapс использованием лямбды(k, v) -> ...;merge(K, V, Function<V, V, V>)- объединение элементов карты с помощью ключаK. Поиск выполняется поK- если ничего не найдено просто выполняет методput. Если найдено, то вызывается функция объединения, гдеv1- старое значение,v2- новое значение (фактически, новый V как параметр),v1- результирующее значение (то, что будет записано вMap);put(K, V)- стандартная вставка. Метод возвращает старое значение илиnull(если по ключу ничего не было);putIfAbsent(K, V)- аналогput, но если вMapтакой ключ уже существует, то ничего не записывает, а просто возвращает текущее значение;computeIfAbsent(K, Function<K, V>)- если по ключу нет значения, или оно не имеет значения (null), то делает вычисление и выполняетput;computeIfPresent(K, Function<K, V, V>)- если по ключу есть значение (в методе нет проверки значения наnull, т.е. еслиVравенnull, то вычисление выполнится для объекта с ключомnullвMap), выполняет вычисления и обновляет значение вMap;compute(K, BiFunction<K, V, V>)- комбинация обоих методов выше - выполняет вычисление всегда. Если метод вычисления возвращает пустое значение (null), то элемент по ключу будет удален изMap.
HashMap
Сортировка по hashCode. Так, например, для ключа Integer сортировка будет по возрастанию:
1 | Map<Integer, String> map = new HashMap<Integer, String>(); |
Это не сработает для Long и с более высокоразрядными числами (объектами).
HashMap реализует быстрый поиск, но для этого рекомендуется использовать ключ, реализующий Comparable.
ConcurrentHashMap
reduce*,reduceKeys*,reduceValues*- метод выполняет сведение значений или ключей в некое общее значение. Очень удобно для калькуляции чего-либо:
1 | ConcurrentHashMap<String, Integer> map = ... |
mappingCount- аналогиченsize(), но возвращает неint, аlong;search- выполняется поиск первого попавшегося элемента, используя переданную функцию. Если функция возвращаетnull, то считается что элемент не соответствует отбору (элемент пропускается); если метод возвращает неnull, то результатом будет вернувшееся значение:
1 | ConcurrentHashMap<String, Integer> map = ... |
Методы
reduce*иsearch*не блокируют работу сConcurrentHashMap!
Методы принимают количество параллельных задач, но это не значит что все они сразу встают в очередь на исполнение. Используется сложный подсчет, который основывается на подсчете оптимального количества задач для обхода всех ветвей (красно-черного дерева).
IdentityHashMap
В отличае от HashMap, для проверки идентичности ключей использует проверку на ссылочное равенство (не выполняет equals), чем может значительно помочь в ускорении работы с Map. При этом, выбор bucket все так же основан на вычислении hashCode. Тем не менее, ввиду указанной особенности, следует с осторожностью использовать данную реализацию.
Сортировка Map
Поскольку HashMap (например, как наиболее частая реализация) использует отсортированный по hashCode список, то, например сортировка строк зависит от символов в ключе.
В данном случае, hashCode будет считаться для строки, а значит, зависеть от очередности расположения символов в Unicode таблице:
- Числа
- Char в верхнем регистре
- Char в нижнем регистре
Кастомизация сортировки
Собственную реализацию сортировки можно достичь несколькими путями:
- путем реализации собственного компаратора;
- путем реализации интерфейса Comparable для целевого типа.
Сортировка в обратном порядке
Для простой сортировки в обратном порядке можно воспользоваться, например, следующими техниками:
- Подмена операндов сравнения:
1 | (x, y) -> x.compareTo(y); // asc |
- Умножение на
-1:
1 | (x, y) -> x.compareTo(y); // asc |
При реализации метода Comparable необходимо следить за связанностью и непротиворечивостью методов compareTo, equals и hashCode! Если compareTo возвращает 0, то equals должен вернуть true; если compareTo возвращает значение отличное от 0, то equals должен вернуть false.
Comparator
В связи с правилом, изложенным выше, иногда проще использовать локализованную реализацию сортировки через Comparator - он не зависит от equals сравниваемого типа:
1 | var custom = new Comparator<String>() { |
В последнем примере будет выполнена стандартная сортировка для объекта типа Double, которая будет результатом выполнения метода функционального интерфейса, переданного в аргументе. Так, например, для (50, 30) будет выполнено преобразование в Long и уже для Long будут выполнены их стандартные методы compareTo (от интерфейса Comparable).
Можно выполнять последовательную сортировку (конвейерную):
1 | Comparator<String> x = Comparator.comparing(String::trim).reversed().comparing(String::length); |
Каждый вызов reversed меняет направление сортировки. А для установки первоначального направления сортировки можно выполнить:
1 | Comparator.reversedOrder().thenComparing(...)... |
Коллекция TreeSet требует (как и TreeMap) реализации интерфейса Comparable, иначе при выполнении вставки (а значит, и при выполнении сортировки) будет выбрасываться исключение ClassCastException (попытка преобразования в Comparable). Это можно избежать, если в конструкторе передать измененный Comparator.
Присутствуют заготовленные компараторы MapEntry.compareByKey/compareByValue:
1 | Map.entrySet().stream().sorted(MapEntry::compareByValue); |
merge
Метод merge работает аналогичным образом, что и putIfAbsent, но предоставляет возможность выбора значения для вставки в случае если в Map значение с подобным ключом уже существует:
1 | var persons = new HashMap<String, Integer>(); |