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

Руководство по группировке Java 8От Collector

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

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Введение

В этом руководстве мы увидим, как работает сборщик groupingBy , на различных примерах.

Чтобы понять материал, изложенный в этом руководстве, нам потребуются базовые знания о функциях Java 8. Мы можем взглянуть на введение в потоки Java 8 и руководство по коллекторам Java 8 для этих основ.

  2.10.  Объединение нескольких атрибутов сгруппированного результата   

2. группировкаПо коллекционерам

Java 8 Stream API позволяет нам обрабатывать наборы данных декларативным способом.

Статические фабричные методы Collectors.groupingBy() и Collectors.groupingByConcurrent() предоставляют нам функциональные возможности, аналогичные предложению GROUP BY в языке SQL. Мы используем их для группировки объектов по некоторому свойству и сохранения результатов в экземпляре карты .

Перегруженные методы groupingBy :

Во-первых, с функцией классификации в качестве параметра метода:

static <T,K> Collector<T,?,Map<K,List<T>>> 
groupingBy(Function<? super T,? extends K> classifier)

Во-вторых, с функцией классификации и вторым коллектором в качестве параметров метода:

static <T,K,A,D> Collector<T,?,Map<K,D>>
groupingBy(Function<? super T,? extends K> classifier,
Collector<? super T,A,D> downstream)

Наконец, с функцией классификации, методом поставщика (который предоставляет реализацию карты , которая содержит конечный результат) и вторым коллектором в качестве параметров метода:

static <T,K,D,A,M extends Map<K,D>> Collector<T,?,M>
groupingBy(Function<? super T,? extends K> classifier,
Supplier<M> mapFactory, Collector<? super T,A,D> downstream)

2.1. Пример настройки кода

Чтобы продемонстрировать использование groupingBy() , давайте определим класс BlogPost (мы будем использовать поток объектов BlogPost ):

class BlogPost {
String title;
String author;
BlogPostType type;
int likes;
}

Далее, BlogPostType :

enum BlogPostType {
NEWS,
REVIEW,
GUIDE
}

Затем список объектов BlogPost :

List<BlogPost> posts = Arrays.asList( ... );

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

class Tuple {
BlogPostType type;
String author;
}

2.2. Простая группировка по одному столбцу

Начнем с простейшего метода groupingBy , который принимает в качестве параметра только классификационную функцию. Функция классификации применяется к каждому элементу потока.

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

Чтобы сгруппировать сообщения блога в списке сообщений блога по их типу :

Map<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType));

2.3. groupingBy со сложным типом ключа карты

Функция классификации не ограничивается возвратом только скалярного или строкового значения. Ключом результирующей карты может быть любой объект, если мы убедимся, что реализуем необходимые методы equals и hashcode .

Для группировки с использованием двух полей в качестве ключей мы можем использовать класс Pair , предоставленный в пакетах javafx.util или org.apache.commons.lang3.tuple .

Например, чтобы сгруппировать сообщения блога в списке по типу и автору, объединенным в экземпляре Apache Commons Pair :

Map<Pair<BlogPostType, String>, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(groupingBy(post -> new ImmutablePair<>(post.getType(), post.getAuthor())));

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

Map<Tuple, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(groupingBy(post -> new Tuple(post.getType(), post.getAuthor())));

Java 16 представила концепцию записи как новую форму создания неизменяемых классов Java.

Функция записи предоставляет нам более простой, понятный и безопасный способ группировки по, чем Tuple. Например, мы определили экземпляр записи в BlogPost :

public class BlogPost {
private String title;
private String author;
private BlogPostType type;
private int likes;
record AuthPostTypesLikes(String author, BlogPostType type, int likes) {};

// constructor, getters/setters
}

Теперь очень просто сгруппировать BlotPost в списке по типу, автору и лайкам с помощью экземпляра записи :

Map<BlogPost.AuthPostTypesLikes, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(groupingBy(post -> new BlogPost.AuthPostTypesLikes(post.getAuthor(), post.getType(), post.getLikes())));

2.4. Изменение возвращаемого типа значения карты

Вторая перегрузка groupingBy использует дополнительный второй сборщик (нисходящий сборщик), который применяется к результатам первого сборщика.

Когда мы указываем функцию классификации, но не нижестоящий сборщик, за кулисами используется сборщик toList() .

Давайте используем сборщик toSet() в качестве нижестоящего сборщика и получим набор сообщений блога (вместо списка ):

Map<BlogPostType, Set<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, toSet()));

2.5. Группировка по нескольким полям

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

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

Map<String, Map<BlogPostType, List>> map = posts.stream()
.collect(groupingBy(BlogPost::getAuthor, groupingBy(BlogPost::getType)));

2.6. Получение среднего из сгруппированных результатов

Используя нижестоящий сборщик, мы можем применять функции агрегирования к результатам функции классификации.

Например, чтобы найти среднее количество лайков для каждого типа сообщения в блоге :

Map<BlogPostType, Double> averageLikesPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, averagingInt(BlogPost::getLikes)));

2.7. Получение суммы из сгруппированных результатов

Чтобы рассчитать общую сумму лайков для каждого типа :

Map<BlogPostType, Integer> likesPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, summingInt(BlogPost::getLikes)));

2.8. Получение максимума или минимума из сгруппированных результатов

Еще одна агрегация, которую мы можем выполнить, — это получить пост в блоге с максимальным количеством лайков:

Map<BlogPostType, Optional<BlogPost>> maxLikesPerPostType = posts.stream()
.collect(groupingBy(BlogPost::getType,
maxBy(comparingInt(BlogPost::getLikes))));

Точно так же мы можем применить нижестоящий сборщик minBy , чтобы получить запись в блоге с минимальным количеством лайков .

Обратите внимание, что коллекторы maxBy и minBy учитывают возможность того, что коллекция, к которой они применяются, может быть пустой. Вот почему тип значения на карте — Optional<BlogPost> .

2.9. Получение сводки по атрибуту сгруппированных результатов

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

Давайте посчитаем сводку по атрибуту лайков сообщений блога для каждого типа:

Map<BlogPostType, IntSummaryStatistics> likeStatisticsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
summarizingInt(BlogPost::getLikes)));

Объект IntSummaryStatistics для каждого типа содержит количество, сумму, среднее, минимальное и максимальное значения для атрибута лайков . Для двойных и длинных значений существуют дополнительные сводные объекты.

2.10. Объединение нескольких атрибутов сгруппированного результата

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

Первый подход заключается в использовании Collectors::collectingAndThen для нижестоящего сборщика groupingBy . Для первого параметра collectAndThen мы собираем поток в список, используя Collectors::toList . Второй параметр применяет завершающее преобразование, мы можем использовать его с любым из методов класса Collectors , поддерживающих агрегацию, для получения желаемых результатов.

Например, сгруппируем по авторам и для каждого подсчитаем количество заголовков , перечислим заголовки и предоставим сводную статистику лайков . Для этого мы начинаем с добавления новой записи в BlogPost :

public class BlogPost {
// ...
record PostCountTitlesLikesStats(long postCount, String titles, IntSummaryStatistics likesStats){};
// ...
}

Реализация groupingBy и collectAndThen будет такой:

Map<String, BlogPost.PostcountTitlesLikesStats> postsPerAuthor = posts.stream()
.collect(groupingBy(BlogPost::getAuthor, collectingAndThen(toList(), list -> {
long count = list.stream()
.map(BlogPost::getTitle)
.collect(counting());
String titles = list.stream()
.map(BlogPost::getTitle)
.collect(joining(" : "));
IntSummaryStatistics summary = list.stream()
.collect(summarizingInt(BlogPost::getLikes));
return new BlogPost.PostcountTitlesLikesStats(count, titles, summary);
})));

В первом параметре collectAndThen мы получаем список BlogPost . Мы используем его в заключительном преобразовании в качестве входных данных для лямбда-функции для вычисления значений для создания PostCountTitlesLikesStats .

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

BlogPost.PostcountTitlesLikesStats result = postsPerAuthor.get("Author 1");
assertThat(result.postCount()).isEqualTo(3L);
assertThat(result.titles()).isEqualTo("News item 1 : Programming guide : Tech review 2");
assertThat(result.likesStats().getMax()).isEqualTo(20);
assertThat(result.likesStats().getMin()).isEqualTo(15);
assertThat(result.likesStats().getAverage()).isEqualTo(16.666d, offset(0.001d));

Мы также можем выполнять более сложные агрегации, если используем Collectors::toMap для сбора и агрегирования элементов потока .

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

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

public class BlogPost {
// ...
record TitlesBoundedSumOfLikes(String titles, int boundedSumOfLikes) {};
// ...
}

Затем мы группируем и накапливаем поток следующим образом:

int maxValLikes = 17;
Map<String, BlogPost.TitlesBoundedSumOfLikes> postsPerAuthor = posts.stream()
.collect(toMap(BlogPost::getAuthor, post -> {
int likes = (post.getLikes() > maxValLikes) ? maxValLikes : post.getLikes();
return new BlogPost.TitlesBoundedSumOfLikes(post.getTitle(), likes);
}, (u1, u2) -> {
int likes = (u2.boundedSumOfLikes() > maxValLikes) ? maxValLikes : u2.boundedSumOfLikes();
return new BlogPost.TitlesBoundedSumOfLikes(u1.titles().toUpperCase() + " : " + u2.titles().toUpperCase(), u1.boundedSumOfLikes() + likes);
}));

Первый параметр toMap группирует ключи, применяющие BlogPost::getAuthor .

Второй параметр преобразует значения карты с помощью лямбда-функции для преобразования каждого сообщения блога в запись TitlesBoundedSumOfLikes . ``

Третий параметр toMap имеет дело с повторяющимися элементами для данного ключа, и здесь мы используем другую лямбда-функцию для объединения заголовков и суммирования лайков с максимально допустимым значением, указанным в maxValLikes .

2.11. Сопоставление сгруппированных результатов с другим типом

Мы можем добиться более сложных агрегаций, применив сборщик отображения нижестоящего потока к результатам функции классификации.

Давайте получим конкатенацию заголовков сообщений для каждого типа сообщений в блоге :

Map<BlogPostType, String> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
mapping(BlogPost::getTitle, joining(", ", "Post titles: [", "]"))));

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

2.11. Изменение типа карты возврата

При использовании сборщика groupingBy мы не можем делать предположения о типе возвращаемого Map . Если мы хотим уточнить, какой тип карты мы хотим получить из группы, то мы можем использовать третий вариант метода groupingBy , который позволяет нам изменить тип карты , передав функцию поставщика карты .

Давайте получим EnumMap , передав функцию поставщика EnumMap методу groupingBy :

EnumMap<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
() -> new EnumMap<>(BlogPostType.class), toList()));

3. Параллельная группировкаПо сборщику

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

Чтобы выполнить операцию группировки одновременно, поток должен быть параллельным:

ConcurrentMap<BlogPostType, List<BlogPost>> postsPerType = posts.parallelStream()
.collect(groupingByConcurrent(BlogPost::getType));

Если мы решим передать функцию поставщика карты сборщику groupingByConcurrent , то нам нужно убедиться, что функция возвращает либо ConcurrentHashMap , либо его подкласс.

4. Дополнения для Java 9

В Java 9 появились два новых сборщика, которые хорошо работают с groupingBy ; больше информации о них можно найти здесь .

5. Вывод

В этой статье мы рассмотрели использование сборщика groupingBy , предлагаемого API коллекторов Java 8 .

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

Полную реализацию примеров из этой статьи можно найти в проекте GitHub .