Рубрики
Без рубрики

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

В статье рассматриваются коллекторы Java 8, показаны примеры встроенных коллекторов, а также показано, как создать пользовательский коллектор.

Автор оригинала: Grzegorz Piwowarek.

1. Обзор

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

Если вы хотите узнать больше о самом API Stream , проверьте эту статью .

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

Дальнейшее чтение:

Учебник по потоковому API Java 8

Руководство по Java 8 groupingBy Collector

Новые сборщики потоков в Java 9

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

Stream.collect () – это один из терминальных методов Java 8 Stream API . Это позволяет нам выполнять изменяемые операции свертки (переупаковка элементов в некоторые структуры данных и применение некоторой дополнительной логики, их объединение и т. Д.) Для элементов данных, хранящихся в экземпляре 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 givenList = Arrays.asList("a", "bb", "ccc", "dd");

3.1. Коллекторы.тоЛист()

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

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

List result = givenList.stream()
  .collect(toList());

3.1.1. Collectors.to НемодифицируЕмый список()

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

List result = givenList.stream()
  .collect(toUnmodifiableList());

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

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

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

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

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

Set result = givenList.stream()
  .collect(toSet());

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

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

3.2.1. Коллекторы.toUnmodifiableSet()

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

Set result = givenList.stream()
  .collect(toUnmodifiableSet());

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

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

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

Как вы, вероятно, уже заметили, при использовании для набора и перечисления коллекторов вы не можете делать никаких предположений об их реализации. Если вы хотите использовать пользовательскую реализацию, вам нужно будет использовать to Collection collector с предоставленной коллекцией по вашему выбору.

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

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

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

3.4. Коллекционеры.toMap()

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

  • кейМаппер
  • valueMapper

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

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

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

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

Что произойдет, если наша коллекция содержит повторяющиеся элементы? В отличие от to Set , to Map не фильтрует дубликаты молча. Это понятно – как он должен выяснить, какое значение выбрать для этого ключа?

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

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

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

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

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

3.4.1. Collectors.to Немодифицируемая карта()

Аналогично тому, как для List s и Set s, Java 10 представила простой способ сбора потока элементов в неизменяемую карту :

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

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

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

3.5. Коллекционеры.Коллекционирование и затем()

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

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

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

3.6. Коллекторы.присоединение()

Соединение collector может использоваться для соединения Stream элементов.

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

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. Коллекторы.подсчет()

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

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

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

3.8. Коллекторы.суммирование Double/Long/Int()

Summarizing Double/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. Коллекторы.Усредненные/Длинные/Int()

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

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

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

3.10. Коллекторы.суммирование Double/Long/Int()

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

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

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

3.11. Коллекционеры.maxBy()/minBy()

maxBy /|/minBy коллекторы возвращают самый большой/самый маленький элемент потока в соответствии с предоставленным экземпляром Компаратора .

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

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

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

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

groupingBy collector используется для группировки объектов по какому-либо свойству и хранения результатов в экземпляре Map .

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

Map> 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 , и вы можете использовать любой Collector по вашему выбору.

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

partitioningBy – это специализированный случай группировки по , который принимает Предикат экземпляр и собирает Поток элементы в Карту экземпляр, который хранит Логические значения в качестве ключей и коллекции в качестве значений. Под ключом “true” вы можете найти коллекцию элементов, соответствующих данному Предикату , а под ключом “false” вы можете найти коллекцию элементов, не соответствующих данному Предикату .

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

Map> result = givenList.stream()
  .collect(partitioningBy(s -> s.length() > 2))

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

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

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

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

List numbers = Arrays.asList(42, 4, 2, 24);
Optional min = numbers.stream().collect(minBy(Integer::compareTo));
Optional 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. Пользовательские коллекторы

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

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

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

private class ImmutableSetCollector
  implements Collector, ImmutableSet> {...}

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

  • Поставщик> поставщик ()
  • BiConsumer, T> аккумулятор ()
  • Двоичный оператор> комбайнер ()
  • Функция, ImmutableSet> финишер ()
  • Набор<Характеристик> характеристики ()

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

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

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

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

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

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

Метод finisher() возвращает функцию, которая используется для преобразования накопителя в тип конечного результата, поэтому в этом случае мы просто будем использовать метод Builder ‘s build :

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

Метод characteristics() используется для предоставления потоку некоторой дополнительной информации, которая будет использоваться для внутренней оптимизации. В этом случае мы не обращаем внимания на порядок элементов в наборе |, поэтому будем использовать характеристики .НЕУПОРЯДОЧЕННЫЙ . Чтобы получить дополнительную информацию по этому вопросу, проверьте Характеристики ‘ JavaDoc.

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

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

public class ImmutableSetCollector
  implements Collector, ImmutableSet> {

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

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

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

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

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

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

и вот в действии:

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

ImmutableSet result = givenList.stream()
  .collect(toImmutableSet());

5. Заключение

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

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