Ранее мы уже несколько раз использовали метод Collect() для объединения элементов, возвращаемых Stream, в ArrayList. Это операция сокращения, которая полезна для преобразования коллекции в другой тип (обычно изменяемую коллекцию). Функция Collect(), если она используется в сочетании с некоторыми методами класса инструментов Collectors, может обеспечить большое удобство, как мы покажем в этом разделе.
Давайте продолжим использовать предыдущий список Person в качестве примера, чтобы увидеть, на что способен метод Collect(). Предположим, мы хотим найти всех людей старше 20 лет из исходного списка. Вот версия, реализованная с использованием изменяемости и метода forEach():
Скопируйте код кода следующим образом:
List<Person> oldThan20 = новый ArrayList<>();
.filter(person -> person.getAge() > 20)
.forEach(person -> oldThan20.add(person)); System.out.println("Люди старше 20: " + oldThan20);
Мы используем метод filter(), чтобы отфильтровать из списка всех людей старше 20 лет. Затем в методе forEach мы добавляем элементы в ArrayList, который был инициализирован ранее. Давайте сначала посмотрим на вывод этого кода, а затем восстановим его позже.
Скопируйте код кода следующим образом:
Люди старше 20 лет: [Сара — 21, Джейн — 21, Грег — 35]
Вывод программы правильный, но все еще есть небольшая проблема. Во-первых, добавление элементов в коллекцию — это операция низкого уровня — она императивна, а не декларативна. Если мы хотим преобразовать эту итерацию в параллельную, мы должны учитывать проблемы безопасности потоков — изменчивость затрудняет распараллеливание. К счастью, эту проблему можно легко решить с помощью метода Collect(). Давайте посмотрим, как это достигается.
Метод Collect() принимает поток и собирает его в контейнер результатов. Для этого ему нужно знать три вещи:
+ Как создать контейнер результатов (например, с помощью метода ArrayList::new) + Как добавить в контейнер одиночный элемент (например, с помощью метода ArrayList::add) + Как объединить один набор результатов с другим (например, с помощью метода ArrayList::addAll)
Последний пункт не требуется для последовательных операций; код предназначен для поддержки как последовательных, так и параллельных операций.
Мы предоставляем эти операции методу Collect и позволяем ему собирать отфильтрованный поток.
Скопируйте код кода следующим образом:
Список<Человек> старшеThan20 =
люди.поток()
.filter(person -> person.getAge() > 20)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
System.out.println("Люди старше 20: " + oldThan20);
Результат этого кода такой же, как и раньше, но такой способ написания имеет множество преимуществ.
Прежде всего, наш метод программирования более целенаправленный и выразительный, четко передающий цель сбора результатов в ArrayList. Первый параметр функции Collect() — это фабрика или производитель, а следующий параметр — это операция, используемая для сбора элементов.
Во-вторых, поскольку мы не выполняем явных изменений в коде, мы легко можем выполнить эту итерацию параллельно. Мы позволяем базовой библиотеке обрабатывать изменения, и она позаботится о проблемах координации и безопасности потоков, хотя сам ArrayList не является потокобезопасным — отличная работа.
Если позволяют условия, метод Collect() может параллельно добавлять элементы в разные подсписки, а затем объединять их в большой список потокобезопасным способом (последний параметр используется для операции слияния).
Мы увидели, что использование метода Collect() имеет множество преимуществ по сравнению с добавлением элементов в список вручную. Давайте рассмотрим перегруженную версию этого метода — она проще и удобнее — в качестве параметра принимает Collector. Этот Collector представляет собой интерфейс, включающий производителей, сумматоры и объединители. В предыдущих версиях эти операции передавались в методы как независимые параметры. Использование Collector проще и может использоваться повторно. Класс инструмента Collectors предоставляет метод toList, который может генерировать реализацию Collector для добавления элементов в ArrayList. Давайте изменим предыдущий код и воспользуемся методом Collect().
Скопируйте код кода следующим образом:
Список<Человек> старшеThan20 =
люди.поток()
.filter(person -> person.getAge() > 20)
.collect(Коллекторы.toList());
System.out.println("Люди старше 20: " + oldThan20);
Используется краткая версия метода Collect() класса инструментов Collectors, но ее можно использовать более чем одним способом. В классе инструментов Collectors имеется несколько различных методов для выполнения различных операций сбора и сложения. Например, в дополнение к методу toList() существует также метод toSet(), который можно добавить в Set, метод toMap(), который можно использовать для сбора в набор значений ключа, и метод toMap(), который можно использовать для сбора в набор значений ключа. joining(), который можно склеить в строку. Мы также можем комбинировать для использования такие методы, как Mapping(), CollectAndThen(), minBy(), MaxBy() и GroupingBy().
Давайте воспользуемся методом groupingBy() для группировки людей по возрасту.
Скопируйте код кода следующим образом:
Map<Integer, List<Person>>peopleByAge =
люди.поток()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println("Сгруппировано по возрасту: " +peopleByAge);
Просто вызовите метод Collect(), чтобы завершить группировку. groupingBy() принимает лямбда-выражение или ссылку на метод — это называется функцией классификации — и возвращает значение определенного атрибута объекта, который необходимо сгруппировать. Согласно значению, возвращаемому нашей функцией, элементы в вызывающем контексте помещаются в определенную группу. Результаты группировки можно увидеть в выводе:
Скопируйте код кода следующим образом:
Сгруппировано по возрасту: {35=[Грег - 35], 20=[Джон - 20], 21=[Сара - 21, Джейн - 21]}
Люди были сгруппированы по возрасту.
В предыдущем примере мы сгруппировали людей по возрасту. Вариант метода groupingBy() может группироваться по нескольким условиям. Простой метод groupingBy() использует классификатор для сбора элементов. Общий сборщик groupingBy() может указать сборщик для каждой группы. Другими словами, в процессе сбора элементы будут проходить через разные классификаторы и коллекции, как мы увидим ниже.
Продолжая приведенный выше пример, на этот раз вместо группировки по возрасту мы просто получаем имена людей и сортируем их по возрасту.
Скопируйте код кода следующим образом:
Map<Integer, List<String>> nameOfPeopleByAge =
люди.поток()
.собирать(
groupingBy(Person::getAge, маппинг(Person::getName, toList())));
System.out.println("Люди сгруппированы по возрасту: " + nameOfPeopleByAge);
Эта версия groupingBy() принимает два параметра: первый — возраст, который является условием группировки, а второй — коллектор, который является результатом, возвращаемым функцией сопоставления(). Все эти методы взяты из класса инструментов Collectors и статически импортированы в этот код. Метод сопоставления() принимает два параметра: один — это атрибут, используемый для сопоставления, а другой — место, где должны быть собраны объекты, например список или набор. Давайте посмотрим на вывод приведенного выше кода:
Скопируйте код кода следующим образом:
Люди сгруппированы по возрасту: {35=[Грег], 20=[Джон], 21=[Сара, Джейн]}
Как видите, имена людей сгруппированы по возрасту.
Давайте еще раз рассмотрим операцию комбинирования: группируем по первой букве имени, а затем выбираем самого старшего человека в каждой группе.
Скопируйте код кода следующим образом:
Comparator<Person> byAge = Comparator.comparing(Person::getAge);
Map<Character, Необязательно<Person>> oldPersonOfEachLetter =
люди.поток()
.collect(groupingBy(person -> person.getName().charAt(0),
сокращение(BinaryOperator.maxBy(byAge))));
System.out.println("Самый старый человек каждой буквы:");
System.out.println(oldestPersonOfEachLetter);
Сначала мы отсортировали имена в алфавитном порядке. Для этого мы передаем лямбда-выражение в качестве первого параметра groupingBy(). Это лямбда-выражение используется для возврата первой буквы имени для группировки. Второй параметр больше не является Mapping(), а выполняет операцию сокращения. Внутри каждой группы он использует метод maxBy() для получения самого старого элемента из всех элементов. Синтаксис выглядит немного раздутым из-за множества операций, которые он объединяет, но в целом он выглядит следующим образом: Группировать по первой букве имени, а затем переходить к старшему в группе. Рассмотрим выходные данные этого кода, в котором перечислен самый старый человек в группе имен, начинающихся с заданной буквы.
Скопируйте код кода следующим образом:
Самый старый человек каждой буквы:
{S=Необязательно [Сара – 21], G = Необязательно [Грег – 35], J = Необязательно [Джейн – 21]}
Мы уже испытали возможности метода Collect() и служебного класса Collectors. В официальной документации вашей IDE или JDK потратьте некоторое время на изучение класса инструментов Collectors и ознакомьтесь с различными методами, которые он предоставляет. Далее мы будем использовать лямбда-выражения для реализации некоторых фильтров.