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> {...}
- T – тип объектов, которые будут доступны для сбора
- A – тип изменяемого объекта-аккумулятора
- 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 . Больше интересных статей можно прочитать на моем сайте .