Перейти к основному содержимому

Руководство по сборщикам Java 8

· 10 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим коллекторы Java 8, которые используются на последнем этапе обработки Stream .

Чтобы узнать больше о самом Stream API, мы можем прочитать эту статью .

Если мы хотим увидеть, как использовать возможности Collectors для параллельной обработки, мы можем посмотреть на этот проект.

2. Метод Stream.collect ()

Stream.collect() — это один из терминальных методов Stream API Java 8 . Это позволяет нам выполнять изменяемые операции свертки (переупаковывать элементы в некоторые структуры данных и применять некоторую дополнительную логику, объединять их и т. д.) над элементами данных, хранящимися в экземпляре Stream .

Стратегия для этой операции предоставляется через реализацию интерфейса Collector .

3. Коллекционеры

Все предопределенные реализации можно найти в классе Collectors . Обычной практикой является использование с ними следующего статического импорта для повышения читабельности:

import static java.util.stream.Collectors.*;

Мы также можем использовать отдельные сборщики импорта по нашему выбору:

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

В следующих примерах мы будем повторно использовать следующий список:

List<String> givenList = Arrays.asList("a", "bb", "ccc", "dd");

3.1. Сборщики.toList()

Сборщик toList можно использовать для сбора всех элементов Stream в экземпляр List . Важно помнить, что мы не можем предполагать какую-либо конкретную реализацию List с помощью этого метода. Если мы хотим иметь больше контроля над этим, мы можем вместо этого использовать toCollection .

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

List<String> result = givenList.stream()
.collect(toList());

3.1.1. Collectors.toUnmodifiedList()

Java 10 представила удобный способ накапливать элементы Stream в неизменяемый список :

List<String> result = givenList.stream()
.collect(toUnmodifiableList());

Теперь, если мы попытаемся изменить список результатов , мы получим исключение UnsupportedOperationException :

assertThatThrownBy(() -> result.add("foo"))
.isInstanceOf(UnsupportedOperationException.class);

3.2. Коллекторы.toSet()

Сборщик toSet можно использовать для сбора всех элементов Stream в экземпляр Set . Важно помнить, что мы не можем предполагать какую-либо конкретную реализацию Set с помощью этого метода. Если мы хотим иметь больше контроля над этим, мы можем вместо этого использовать toCollection .

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

Set<String> result = givenList.stream()
.collect(toSet());

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

List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
Set<String> result = listWithDuplicates.stream().collect(toSet());
assertThat(result).hasSize(4);

3.2.1. Collectors.toUnmodifiedSet()

Начиная с Java 10, мы можем легко создать неизменяемый набор с помощью сборщика toUnmodifiedSet() :

Set<String> result = givenList.stream()
.collect(toUnmodifiableSet());

Любая попытка изменить результирующий набор приведет к исключению UnsupportedOperationException :

assertThatThrownBy(() -> result.add("foo"))
.isInstanceOf(UnsupportedOperationException.class);

3.3. Коллекционеры.toCollection()

Как мы уже отмечали, при использовании сборщиков toSet и toList мы не можем делать никаких предположений об их реализации. Если мы хотим использовать пользовательскую реализацию, нам нужно использовать сборщик toCollection с предоставленной коллекцией по нашему выбору.

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

List<String> result = givenList.stream()
.collect(toCollection(LinkedList::new))

Обратите внимание, что это не будет работать с неизменяемыми коллекциями. В таком случае нам нужно будет либо написать собственную реализацию Collector , либо использовать collectAndThen .

3.4. Коллекционеры . для отображения()

Сборщик toMap можно использовать для сбора элементов Stream в экземпляр Map . Для этого нам нужно предоставить две функции:

  • keyMapper
  • значениемаппер

Мы будем использовать keyMapper для извлечения ключа Map из элемента Stream и valueMapper для извлечения значения, связанного с данным ключом.

Давайте соберем эти элементы в карту , в которой строки хранятся как ключи, а их длины — как значения:

Map<String, Integer> result = givenList.stream()
.collect(toMap(Function.identity(), String::length))

Function.identity() — это просто ярлык для определения функции, которая принимает и возвращает одно и то же значение.

Итак, что произойдет, если наша коллекция содержит повторяющиеся элементы? В отличие от toSet , toMap не фильтрует дубликаты молча, что понятно, потому что как понять, какое значение выбрать для этого ключа?

List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
assertThatThrownBy(() -> {
listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));
}).isInstanceOf(IllegalStateException.class);

Обратите внимание, что toMap даже не оценивает, равны ли значения. Если он видит повторяющиеся ключи, он немедленно выдает исключение IllegalStateException .

В таких случаях с коллизией ключей мы должны использовать toMap с другой подписью:

Map<String, Integer> result = givenList.stream()
.collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));

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

3.4.1. Collectors.toUnmodifiedMap()

Подобно List и Set , в Java 10 появился простой способ собрать элементы Stream в неизменяемый Map :

Map<String, Integer> result = givenList.stream()
.collect(toMap(Function.identity(), String::length))

Как мы видим, если мы попытаемся поместить новую запись в результат Map , мы получим UnsupportedOperationException :

assertThatThrownBy(() -> result.put("foo", 3))
.isInstanceOf(UnsupportedOperationException.class);

3.5. Коллекторы .c ollectingAndThen()

CollectingAndThen — это специальный сборщик, который позволяет нам выполнить другое действие над результатом сразу после завершения сбора.

Давайте соберем элементы Stream в экземпляр List , а затем преобразуем результат в экземпляр ImmutableList :

List<String> result = givenList.stream()
.collect(collectingAndThen(toList(), ImmutableList::copyOf))

3.6. Коллекционеры .j oining()

Коллектор соединения можно использовать для соединения элементов Stream<String> .

Мы можем объединить их вместе, выполнив:

String result = givenList.stream()
  .collect(joining());

Это приведет к:

"abbcccdd"

Мы также можем указать пользовательские разделители, префиксы, постфиксы:

String result = givenList.stream()
.collect(joining(" "));

Это приведет к:

"a bb ccc dd"

Мы также можем написать:

String result = givenList.stream()
.collect(joining(" ", "PRE-", "-POST"));

Это приведет к:

"PRE-a bb ccc dd-POST"

3.7. Коллекторы .c counting()

Counting — это простой сборщик, который позволяет подсчитывать все элементы Stream .

Теперь мы можем написать:

Long result = givenList.stream()
.collect(counting());

3.8. Коллекторы .s ummarizingDouble/Long/Int()

SummarizingDouble/Long/Int — это сборщик, который возвращает специальный класс, содержащий статистическую информацию о числовых данных в потоке извлеченных элементов.

Мы можем получить информацию о длине строки, выполнив:

DoubleSummaryStatistics result = givenList.stream()
.collect(summarizingDouble(String::length));

В этом случае будет верно следующее:

assertThat(result.getAverage()).isEqualTo(2);
assertThat(result.getCount()).isEqualTo(4);
assertThat(result.getMax()).isEqualTo(3);
assertThat(result.getMin()).isEqualTo(1);
assertThat(result.getSum()).isEqualTo(8);

3.9. Collectors.averagingDouble/Long/Int()

AveragingDouble/Long/Int — это сборщик, который просто возвращает среднее значение извлеченных элементов.

Мы можем получить среднюю длину строки, выполнив:

Double result = givenList.stream()
.collect(averagingDouble(String::length));

3.10. Коллекторы .s ummingDouble/Long/Int()

SummingDouble/Long/Int — это сборщик, который просто возвращает сумму извлеченных элементов.

Мы можем получить сумму всех длин строк, выполнив:

Double result = givenList.stream()
.collect(summingDouble(String::length));

3.11. Коллекторы.maxBy()/minBy()

Сборщики MaxBy / MinBy возвращают самый большой/самый маленький элемент Stream в соответствии с предоставленным экземпляром Comparator .

Мы можем выбрать самый большой элемент, выполнив:

Optional<String> result = givenList.stream()
.collect(maxBy(Comparator.naturalOrder()));

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

3.12. Коллекционеры . группировка по()

Коллектор GroupingBy используется для группировки объектов по некоторому свойству с последующим сохранением результатов в экземпляре карты .

Мы можем сгруппировать их по длине строки и сохранить результаты группировки в экземплярах Set :

Map<Integer, Set<String>> result = givenList.stream()
.collect(groupingBy(String::length, toSet()));

Это приведет к тому, что верно следующее:

assertThat(result)
.containsEntry(1, newHashSet("a"))
.containsEntry(2, newHashSet("bb", "dd"))
.containsEntry(3, newHashSet("ccc"));

Мы видим, что вторым аргументом метода groupingBy является Collector. Кроме того, мы можем использовать любой коллекционер по своему выбору.

3.13. Коллекторы.partitioningBy()

PartitioningBy — это особый случай groupingBy , который принимает экземпляр Predicate , а затем собирает элементы Stream в экземпляр Map , в котором логические значения хранятся как ключи, а коллекции — как значения. Под ключом «true» мы можем найти набор элементов, соответствующих заданному Predicate , а под ключом «false» мы можем найти набор элементов, не соответствующих заданному Predicate .

Мы можем написать:

Map<Boolean, List<String>> result = givenList.stream()
.collect(partitioningBy(s -> s.length() > 2))

В результате получается карта, содержащая:

{false=["a", "bb", "dd"], true=["ccc"]}

3.14. Коллекционеры.teeing()

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

List<Integer> numbers = Arrays.asList(42, 4, 2, 24);
Optional<Integer> min = numbers.stream().collect(minBy(Integer::compareTo));
Optional<Integer> max = numbers.stream().collect(maxBy(Integer::compareTo));
// do something useful with min and max

Здесь мы используем два разных сборщика, а затем объединяем результаты этих двух, чтобы создать что-то значимое. До Java 12, чтобы охватить такие варианты использования, нам приходилось дважды работать с данным потоком , сохранять промежуточные результаты во временные переменные, а затем объединять эти результаты впоследствии.

К счастью, Java 12 предлагает встроенный сборщик, который выполняет эти шаги от нашего имени; все, что нам нужно сделать, это предоставить два коллектора и функцию объединителя.

Поскольку этот новый коллектор направляет данный поток в двух разных направлениях, он называется тройником :

numbers.stream().collect(teeing(
minBy(Integer::compareTo), // The first collector
maxBy(Integer::compareTo), // The second collector
(min, max) -> // Receives the result from those collectors and combines them
));

Этот пример доступен на GitHub в проекте core-java-12 .

4. Пользовательские коллекторы

Если мы хотим написать нашу собственную реализацию Collector, нам нужно реализовать интерфейс Collector и указать три его общих параметра:

public interface Collector<T, A, R> {...}
  1. T – тип объектов, которые будут доступны для сбора
  2. A – тип изменяемого объекта-аккумулятора
  3. R – тип конечного результата

Давайте напишем пример Collector для сбора элементов в экземпляр ImmutableSet . Начнем с указания правильных типов:

private class ImmutableSetCollector<T>
implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> {...}

Поскольку нам нужна изменяемая коллекция для внутренней обработки операций с коллекциями, мы не можем использовать ImmutableSet . Вместо этого нам нужно использовать какую-то другую изменяемую коллекцию или любой другой класс, который мог бы временно накапливать для нас объекты. В этом случае мы воспользуемся ImmutableSet.Builder , и теперь нам нужно реализовать 5 методов:

  • Поставщик<ImmutableSet.Builder<T>> поставщик ()
  • BiConsumer<ImmutableSet.Builder<T>, аккумулятор T> ()
  • Комбинатор BinaryOperator <ImmutableSet.Builder<T>> ()
  • Function<ImmutableSet.Builder<T>, ImmutableSet<T>> финишер ()
  • Установить<Характеристики> характеристики ()

Метод supplier() возвращает экземпляр Supplier , который создает пустой экземпляр аккумулятора. Итак, в этом случае мы можем просто написать:

@Override
public Supplier<ImmutableSet.Builder<T>> supplier() {
return ImmutableSet::builder;
}

Метод accumulator() возвращает функцию, которая используется для добавления нового элемента к существующему объекту - аккумулятору . Итак, давайте просто воспользуемся методом add Builder : ``

@Override
public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() {
return ImmutableSet.Builder::add;
}

Метод Combiner() возвращает функцию, которая используется для объединения двух аккумуляторов:

@Override
public BinaryOperator<ImmutableSet.Builder<T>> combiner() {
return (left, right) -> left.addAll(right.build());
}

Метод Finisher() возвращает функцию, которая используется для преобразования аккумулятора в конечный тип результата. Итак, в этом случае мы просто используем метод сборки Builder : ``

@Override
public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() {
return ImmutableSet.Builder::build;
}

Метод характеристик() используется для предоставления Stream дополнительной информации, которая будет использоваться для внутренней оптимизации. В этом случае мы не обращаем внимания на порядок элементов в наборе , потому что будем использовать Characteristics.UNORDERED . Чтобы получить дополнительную информацию по этому вопросу, проверьте Характеристики ' JavaDoc:

@Override public Set<Characteristics> characteristics() {
return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

Вот полная реализация вместе с использованием:

public class ImmutableSetCollector<T>
implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> {

@Override
public Supplier<ImmutableSet.Builder<T>> supplier() {
return ImmutableSet::builder;
}

@Override
public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() {
return ImmutableSet.Builder::add;
}

@Override
public BinaryOperator<ImmutableSet.Builder<T>> combiner() {
return (left, right) -> left.addAll(right.build());
}

@Override
public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() {
return ImmutableSet.Builder::build;
}

@Override
public Set<Characteristics> characteristics() {
return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

public static <T> ImmutableSetCollector<T> toImmutableSet() {
return new ImmutableSetCollector<>();
}

Наконец, здесь в действии:

List<String> givenList = Arrays.asList("a", "bb", "ccc", "dddd");

ImmutableSet<String> result = givenList.stream()
.collect(toImmutableSet());

5. Вывод

В этой статье мы подробно изучили коллекторы Java 8 и показали, как их реализовать. Обязательно ознакомьтесь с одним из моих проектов, который расширяет возможности параллельной обработки в Java.

Все примеры кода доступны на GitHub . Больше интересных статей можно прочитать на моем сайте .