Автор оригинала: 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;
В следующих примерах мы будем повторно использовать следующий список:
ListgivenList = Arrays.asList("a", "bb", "ccc", "dd");
3.1. Коллекторы.тоЛист()
ToList collector можно использовать для сбора всех элементов Stream в экземпляр List . Важно помнить, что мы не можем предполагать какую-либо конкретную реализацию List с помощью этого метода. Если вы хотите иметь больше контроля над этим, используйте вместо этого to Collection .
Давайте создадим экземпляр Stream , представляющий последовательность элементов, и соберем их в экземпляр List :
Listresult = givenList.stream() .collect(toList());
3.1.1. Collectors.to НемодифицируЕмый список()
Java 10 представила удобный способ накопления элементов Stream в неизменяемый список :
Listresult = givenList.stream() .collect(toUnmodifiableList());
Если мы сейчас попытаемся изменить результат Список , мы получим исключение UnsupportedOperationException :
assertThatThrownBy(() -> result.add("foo")) .isInstanceOf(UnsupportedOperationException.class);
3.2. Коллекторы.()
To Set collector может использоваться для сбора всех элементов Stream в экземпляр Set . Важно помнить, что мы не можем предполагать какую-либо конкретную реализацию Set с помощью этого метода. Если мы хотим иметь больше контроля над этим, мы можем вместо этого использовать для сбора .
Давайте создадим экземпляр Stream , представляющий последовательность элементов, и соберем их в экземпляр Set :
Setresult = givenList.stream() .collect(toSet());
Set не содержит повторяющихся элементов. Если наша коллекция содержит элементы, равные друг другу, они появляются в результирующем Наборе только один раз:
ListlistWithDuplicates = 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:
Setresult = givenList.stream() .collect(toUnmodifiableSet());
Любая попытка изменить результирующий набор приведет к исключению UnsupportedOperationException :
assertThatThrownBy(() -> result.add("foo")) .isInstanceOf(UnsupportedOperationException.class);
3.3. Коллекционеры.()
Как вы, вероятно, уже заметили, при использовании для набора и перечисления коллекторов вы не можете делать никаких предположений об их реализации. Если вы хотите использовать пользовательскую реализацию, вам нужно будет использовать to Collection collector с предоставленной коллекцией по вашему выбору.
Давайте создадим экземпляр Stream , представляющий последовательность элементов, и соберем их в экземпляр LinkedList :
Listresult = givenList.stream() .collect(toCollection(LinkedList::new))
Обратите внимание, что это не будет работать с любыми неизменяемыми коллекциями. В таком случае вам нужно будет либо написать пользовательскую реализацию Collector , либо использовать collecting, а затем .
3.4. Коллекционеры.toMap()
To Map collector может использоваться для сбора потока элементов в экземпляр Map . Для этого нам нужно предоставить две функции:
- кейМаппер
- valueMapper
keyMapper будет использоваться для извлечения ключа Map из элемента Stream , а valueMapper будет использоваться для извлечения значения, связанного с данным ключом.
Давайте соберем эти элементы в Карту , в которой строки хранятся в виде ключей, а их длины-в виде значений:
Mapresult = givenList.stream() .collect(toMap(Function.identity(), String::length))
Function.identity () – это просто ярлык для определения функции, которая принимает и возвращает одно и то же значение.
Что произойдет, если наша коллекция содержит повторяющиеся элементы? В отличие от to Set , to Map не фильтрует дубликаты молча. Это понятно – как он должен выяснить, какое значение выбрать для этого ключа?
ListlistWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb"); assertThatThrownBy(() -> { listWithDuplicates.stream().collect(toMap(Function.identity(), String::length)); }).isInstanceOf(IllegalStateException.class);
Обратите внимание, что toMap даже не оценивает, равны ли значения. Если он видит дубликаты ключей, он немедленно выдает исключение IllegalStateException .
В таких случаях при столкновении ключей мы должны использовать для сопоставления с другой сигнатурой:
Mapresult = givenList.stream() .collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));
Третьим аргументом здесь является двоичный оператор |, в котором мы можем указать, как мы хотим обрабатывать коллизии. В этом случае мы просто выберем любое из этих двух сталкивающихся значений, потому что мы знаем, что одни и те же строки всегда будут иметь одинаковую длину.
3.4.1. Collectors.to Немодифицируемая карта()
Аналогично тому, как для List s и Set s, Java 10 представила простой способ сбора потока элементов в неизменяемую карту :
Mapresult = givenList.stream() .collect(toMap(Function.identity(), String::length))
Как мы видим, если мы попытаемся поместить новую запись в resultMap , мы получим UnsupportedOperationException :
assertThatThrownBy(() -> result.put("foo", 3)) .isInstanceOf(UnsupportedOperationException.class);
3.5. Коллекционеры.Коллекционирование и затем()
Сбор и затем – это специальный сборщик, который позволяет выполнять другое действие над результатом сразу после окончания сбора.
Давайте соберем Stream элементы в экземпляр List , а затем преобразуем результат в экземпляр ImmutableList :
Listresult = 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 коллекторы возвращают самый большой/самый маленький элемент потока в соответствии с предоставленным экземпляром Компаратора .
Мы можем выбрать самый большой элемент, выполнив:
Optionalresult = 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. Коллекционеры.()
Давайте найдем максимальное и минимальное числа из заданного потока , используя коллекторы, которые мы изучили до сих пор:
Listnumbers = 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{...}
- T – тип объектов, которые будут доступны для сбора,
- A – тип объекта изменяемого аккумулятора,
- R – тип конечного результата.
Давайте напишем пример коллектора для сбора элементов в экземпляр ImmutableSet . Мы начнем с определения правильных типов:
private class ImmutableSetCollectorimplements 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 Setcharacteristics() { return Sets.immutableEnumSet(Characteristics.UNORDERED); }
Вот полная реализация вместе с использованием:
public class ImmutableSetCollectorimplements 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<>(); }
и вот в действии:
ListgivenList = Arrays.asList("a", "bb", "ccc", "dddd"); ImmutableSet result = givenList.stream() .collect(toImmutableSet());
5. Заключение
В этой статье мы подробно изучили Java 8 Collectors и показали, как его реализовать. Обязательно проверьте один из моих проектов, который расширяет возможности параллельной обработки в Java.
Все примеры кода доступны на GitHub . Вы можете прочитать более интересные статьи на моем сайте .