Java Basics: Collections

Коллекции

Классы-обертки и коллекции

Использование оберток для примитивов может вызвать проблемы, которые сложно обнаружить:

1
2
3
4
5
6
List<Integer> list = new ArrayList();
list.add(1); // [1]
list.add(Integer.of(2)); // [1,2]
list.add(Integer.of(3)); // [1,2,3]
list.remove(1); // [1,3]
list.remove(Integer.of(3)); // [1]

В примере выше, первый 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
2
3
4
5
List<Integer> list = ...;
list.replaceAll(i -> {
if(i == 4) return i;
else return i*2;
});

Реализации коллекций

  • ArrayList - наиболее часто используемый вид списков. Основной плюс в предсказуемом доступе к элементу. Добавление, замена и удаление заметно медленнее чтения;
  • LinkedList - реализует List и Queue. Быстрый доступ к началу и концу списка, но для доступа к объекту в середине потребуется перебор (до момента обнаружения элемента).

Создание List через фабрики

  • Arrays.asList(varargs) - массив с фиксированной длинной. Конвертирует массив в List. Причем, List получается с фиксированной длинной - не добавить, ни убавить нельзя. Но можно использовать set(int, E). При этом, массив и List станут неразрывно связаны ссылками на объекты:
1
2
3
4
String[] arr = {"abc", "def"};
List<String> list = Arrays.asList(arr);
list.set(1, "123"); // arr = list = {"abc", "123"}
arr[0] = "456"; // arr = list = {"456", "123"}
  • List.of(varargs) - метод создает неизменный (immutable) List, который уже никак не будет связан с оригинальным массивом. Так же, в этом List нельзя не удалить ни добавить, ни изменить (!) состав списка (в т.ч. через set(int, E));
  • List.ofCopy(Collection) - неизменный (immutable) список, который является копией исходной коллекции.
1
2
3
4
5
6
String[] array = {"a", "b", "c"};
List<String> list = Arrays.asList(array);
list.replaceAll(e -> "[%s]".formatted(e));
list.remove("[a]"); // UnsupportedOperationException
list.add("[x]"); // UnsupportedOperationException
list.remove("MISSING");

В последнем вызове 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
2
Map<String, Integer> map1 = Map.of("A", 1, "B", 2, "C", 3);
Map<String, String> map2 = Map.of("key", "val1", "k", "v2");
  • 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
2
3
4
5
6
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "1");
map.put(5, "5");
map.put(2, "2");
map.put(3, "3");
System.out.println(map); // {1=1,2=2,3=3,5=5}

Это не сработает для Long и с более высокоразрядными числами (объектами).

HashMap реализует быстрый поиск, но для этого рекомендуется использовать ключ, реализующий Comparable.

ConcurrentHashMap

  • reduce*, reduceKeys*, reduceValues* - метод выполняет сведение значений или ключей в некое общее значение. Очень удобно для калькуляции чего-либо:
1
2
ConcurrentHashMap<String, Integer> map = ...
map.reduce(Runtime.getRuntime().availableProcessors(), (k, v) -> v, (i1, i2) -> i1 + i2);
  • mappingCount - аналогичен size(), но возвращает не int, а long;
  • search - выполняется поиск первого попавшегося элемента, используя переданную функцию. Если функция возвращает null, то считается что элемент не соответствует отбору (элемент пропускается); если метод возвращает не null, то результатом будет вернувшееся значение:
1
2
ConcurrentHashMap<String, Integer> map = ...
StringBuilder str = map.search(Runtime.getRuntime().availableProcessors(), (k, v) -> k.equals("test") ? new StringBuilder(k + v) : null);

Методы reduce* и search* не блокируют работу с ConcurrentHashMap!

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

IdentityHashMap

В отличае от HashMap, для проверки идентичности ключей использует проверку на ссылочное равенство (не выполняет equals), чем может значительно помочь в ускорении работы с Map. Тем не менее, ввиду указанной особенности, следует с осторожностью использовать данную реализацию.

Сортировка Map

Поскольку HashMap (например, как наиболее частая реализация) использует отсортированный по hashCode список, то, например сортировка строк зависит от символов в ключе.

В данном случае, hashCode будет считаться для строки, а значит, зависеть от очередности расположения символов в Unicode таблице:

  1. Числа
  2. Char в верхнем регистре
  3. Char в нижнем регистре

Кастомизация сортировки

Собственную реализацию сортировки можно достичь несколькими путями:

  • путем реализации собственного компаратора;
  • путем реализации интерфейса Comparable для целевого типа.

Сортировка в обратном порядке

Для простой сортировки в обратном порядке можно воспользоваться, например, следующими техниками:

  • Подмена операндов сравнения:
1
2
(x, y) -> x.compareTo(y); // asc
(x, y) -> y.compareTo(x); // desc
  • Умножение на -1:
1
2
(x, y) -> x.compareTo(y); // asc
(x, y) -> x.compareTo(y) * -1; // desc

При реализации метода Comparable необходимо следить за связанностью и непротиворечивостью методов compareTo, equals и hashCode! Если compareTo возвращает 0, то equals должен вернуть true; если compareTo возвращает значение отличное от 0, то equals должен вернуть false.

Comparator

В связи с правилом, изложенным выше, иногда проще использовать локализованную реализацию сортировки через Comparator - он не зависит от equals сравниваемого типа:

1
2
3
4
5
6
7
8
9
10
var custom = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
};

Collections.sort(myList, custom);
Comparator<Integer> integer = (a, b) -> a.compareTo(b);
Comparator<Double> decimal = Comparator.comparing(Double::longValue);

В последнем примере будет выполнена стандартная сортировка для объекта типа Double, которая будет результатом выполнения метода функционального интерфейса, переданного в аргументе. Так, например, для (50, 30) будет выполнено преобразование в Long и уже для Long будут выполнены их стандартные методы compareTo (от интерфейса Comparable).

Можно выполнять последовательную сортировку (конвейерную):

1
Comparator<String> x = Comparator.comparing(String::trim).reversed().comparing(String::length);

Каждый вызов reversed меняет направление сортировки. А для установки первоначального направления сортировки можно выполнить:

1
2
Comparator.reversedOrder().thenComparing(...)...
Comparator.comparing(...).reversed().thenComparing(...)

Коллекция TreeSet требует (как и TreeMap) реализации интерфейса Comparable, иначе при выполнении вставки (а значит, и при выполнении сортировки) будет выбрасываться исключение ClassCastException (попытка преобразования в Comparable). Это можно избежать, если в конструкторе передать измененный Comparator.

Присутствуют заготовленные компараторы MapEntry.compareByKey/compareByValue:

1
Map.entrySet().stream().sorted(MapEntry::compareByValue);
 Comments
Comment plugin failed to load
Loading comment plugin