Программирование JavaStream APIJava-разработчик серверных приложений

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

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

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Collectors.teeing передаёт каждый элемент сразу двум downstream-коллекторам, затем объединяет их два готовых результата функцией merger. Поэтому источник стрима обходится один раз, хотя для каждого элемента выполняются две операции накопления.

Исторический контекст

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

В Java 12 появился teeing collector, который выразил такой сценарий как стандартную композицию коллекторов. Подход особенно полезен для неизменяемых или одноразовых источников, повторный обход которых невозможен или дорог.

Постановка проблемы

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

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

Подробное решение

teeing принимает два downstream-коллектора и функцию объединения. Во время обхода каждый входной элемент передаётся аккумулятору первого и второго коллектора. В параллельном режиме частичные состояния каждого downstream-коллектора объединяются его собственным combiner.

После завершения накопления оба downstream-коллектора применяют свои finisher-функции. Затем merger получает два финальных результата и создаёт итоговое значение. Таким образом, merger работает не с внутренними аккумуляторами, а с результатами, объявленными типами downstream-коллекторов.

import java.util.*; import java.util.stream.*; record Stats(double average, Optional<Integer> maximum) {} List<Integer> values = List.of(2, 4, 7, 9); Stats stats = values.stream().collect(Collectors.teeing( Collectors.averagingInt(Integer::intValue), Collectors.maxBy(Integer::compareTo), Stats::new ));

Здесь список обходится один раз: первый коллектор вычисляет среднее, второй — максимум. Результат второго коллектора имеет тип Optional<Integer>, поэтому пустой источник обрабатывается явно.

За один проход не означает бесплатную обработку: каждый элемент участвует в двух накоплениях, а промежуточное состояние может требовать суммарно больше памяти. Downstream-коллекторы должны иметь корректные supplier, accumulator и combiner; teeing не исправляет нарушения их контрактов.

Для параллельного стрима корректность также зависит от ассоциативности объединения частичных состояний downstream-коллекторов. Итоговый merger должен корректно работать с двумя финальными результатами, но ему не требуется быть combiner-ом для внутренних аккумуляторов.

Ситуация из практики

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

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

Выбран teeing с averagingInt и maxBy. Каждый элемент читается один раз, стандартные коллекторы отвечают за собственные инварианты, а merger только собирает финальный объект Stats. Компромисс — две операции накопления на элемент и необходимость учитывать Optional для пустого источника.

Что кандидаты часто упускают

  1. Действительно ли teeing выполняет два прохода по источнику?

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

Однако вычислительная работа двух downstream-коллекторов никуда не исчезает. Экономится прежде всего повторное чтение и прохождение источника, а не все затраты обработки.

  1. Как teeing ведёт себя в параллельном стриме?

Каждая параллельная часть получает собственные состояния обоих downstream-коллекторов. При объединении сначала применяются combiner первого коллектора и combiner второго, а после завершения накопления выполняются их finisher-функции и общий merger.

Поэтому downstream-коллекторы должны поддерживать корректное разделение работы и объединение частичных результатов. Если их combiner неассоциативен или аккумулятор небезопасен для предусмотренного режима, teeing не гарантирует корректный результат.

  1. Можно ли через teeing получить результат, если один downstream-коллектор не находит элементов?

Да, но результат зависит от контракта этого коллектора. Например, maxBy возвращает Optional.empty() для пустого источника, а counting возвращает ноль.

Merger обязан учитывать такие значения. teeing не подменяет семантику downstream-коллекторов и не превращает отсутствие результата в произвольное значение.