Коллекции
Классы-обертки и коллекции
Использование оберток для примитивов может вызвать проблемы, которые сложно обнаружить:
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
. Тем не менее, ввиду указанной особенности, следует с осторожностью использовать данную реализацию.
Сортировка 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); |