Руководство по коллекционерам Java 8

Руководство по коллекционерам Java 8

1. обзор

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

Если вы хотите узнать больше о самом APIStream, проверьтеthis article.

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

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

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

3. Collectorsс

Все предопределенные реализации можно найти в классе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. Collectors.toList()с

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

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

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

3.2. Collectors.toSet()с

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

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

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

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

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

3.3. Collectors.toCollection()с

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

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

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

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

3.4. Collectors.toMap()

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

  • keyMapper

  • valueMapper

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

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

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

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

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

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

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

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

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

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

3.5. Collectors.c_ollectingAndThen () _

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

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

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

3.6. Collectors.j_oining () _

КоллекторJoining может использоваться для объединения элементов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. Collectors.c_ounting () _

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

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

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

3.8. Collectors.s_ummarizingDouble/Long/Int()_

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

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

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. Collectors.s_ummingDouble/Long/Int()_

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

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

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

3.11. Collectors.maxBy()/minBy()с

MaxBy/MinBy collectors return the biggest/the smallest element of a Stream according to a provided Comparator instance.

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

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

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

3.12. Collectors.groupingBy()

СборщикGroupingBy используется для группировки объектов по некоторому свойству и сохранения результатов в экземпляре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. Collectors.partitioningBy()с

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

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

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

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

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

3.14. Collectors.teeing()с

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

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, чтобы охватить такие варианты использования, нам приходилось дважды работать с заданнымиStream, сохранять промежуточные результаты во временные переменные, а затем объединять эти результаты.

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

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

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 {...}
  1. T - тип объектов, которые будут доступны для сбора,

  2. A - тип изменяемого объекта-аккумулятора,

  3. R - тип конечного результата.

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

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

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

  • Поставщик >supplier ()

  • BiConsumer , T>accumulator ()

  • BinaryOperator >combiner ()

  • Функция , ImmutableSet >finisher ()

  • Установите <Характеристики>characteristics ()

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

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

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

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

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

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

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

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

МетодThe characteristics() используется для предоставления Stream некоторой дополнительной информации, которая будет использоваться для внутренней оптимизации. В этом случае мы не обращаем внимания на порядок элементов вSet, поэтому мы будем использоватьCharacteristics.UNORDERED. Чтобы получить дополнительную информацию по этому вопросу, проверьте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. Заключение

В этой статье мы подробно изучилиCollectors в Java 8 и показали, как его реализовать. Убедитесь, чтоcheck one of my projects which enhances the capabilities of parallel processing in Java.

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