Java 8 Stream API, часть вторая: map/reduce

mapreduceStream API настолько большая тема, что приходится разбивать её на несколько статей. В первой части были примеры решения классических проблем при работе с коллекциями, а во второй я покажу использование редуцирующих функций.

Map/Reduce

Map/Reduce это в общем-то модный buzzword. На самом деле Map/Reduce это очень простой шаблон проектирования, описывающий работу с наборами данных в два шага: на первом шаге выполняются (параллельные) операции над набором, на втором шаге результаты первого шага объединяются.

Интерфейс Stream определяет методы mapTo*(), которые возвращают особые реализации Stream, имеющие методы average(), sum(), min(), max(), которые выполняют соответствующие арифметические действия над элементами набора данных.

В случае, когда нужно обработать поток, к которому операции mapTo*() неприменимы, например из-за типа данных, можно использовать функцию reduce, которая принимает два параметра: начальное значение, оно же значение по умолчанию, и лямбда-выражение с двумя аргументами: первый хранит результат предыдущего вычисления, второй — текущее значение. Пример — вычисление списка уникальных символов в логинах пользователей:

Коллекторы

Метод collect() дополняет метод reduce(). Он так же является редуцирующим методом, но, в отличие от reduce(), в ходе работы он обновляет значение результата, а не заменяет его.

Обычно метод collect()  используется с уже существующими коллекторами из класса java.util.stream.Collectors, который предоставляет множество стандартных операций.

С один типом коллекторов мы сталкивались раньше, в примере Hello,Streams: объединение потока строк в строку:

Коллектор joining(delimiter, prefix, suffix)  объединяет строки в одну, разделяя их разделителем, результирующая строка будет начинаться с префикса и заканчиваться суффиксом. Существует две упрощённых версии этого коллектора: одна принимает только разделитель, другая не принимает ничего, склеивая строки одна с другой.

Второй и, наверное, самый частой используемый тип коллекторов, это коллекторы в коллекции: Collectors.toCollection(), Collectors.toList(), Collectors.toSet() Первый принимает аргументом лямбда-выражение, возвращающее Collection, остальные два создают контейнеры для данных сами.

Очень похожи на них коллекторы работающие с Map. Проблема с Map в том, что один элемент в ней состоит из двух значений, в то время как в Stream каждый элемент есть одно значение. Поэтому методам toMap() необходимо передавать два лямбда-выражения: одно для создания значения в Map из текущего элемента Stream, другое для создания ключа из него же.

Особенностью toMap() является нетерпимость к повторениям значений ключей. Если при сборке потока в Map бует обнаружено дублирующееся значение ключа, будет выброшено IllegalStateException. А если гарантировать это условие не получается, необходимо использовать расширенный вариант toMap(), в который передаётся дополнительная функция. Эта функция будет вызвана для всех значений с одинаковым ключом и должна их обработать и вернуть единственно верное значение для ключа. Например для примера с возрастами логинов можно объединять логины с одним возрастом:

Третий вариант toMap() принимает ещё один параметр: функцию, возвращающий пустой экземпляр Map, позволяя тем самым указывать конкретный тип контейнера.

К трём методам toMap() есть три дополняющих методов toConcurrentMap(), принимающих теже самые аргументы, но работающие с потокобезопасной ConcurrentMap и реализующие потокобезопасный Collector, что позволяет их использовать с параллельной обработкой потоков, про которую я расскажу в следующей части.

Код примера доступен на github.