В ситуации, когда один стрим должен сформировать единый итог из двух независимых сборов, какой механизм Collectors.teeing позволяет сделать это за один проход?
Collectors.teeing передаёт каждый элемент сразу двум downstream-коллекторам, затем объединяет их два готовых результата функцией merger. Поэтому источник стрима обходится один раз, хотя для каждого элемента выполняются две операции накопления.
Обычные коллекторы решают одну задачу сбора: например, вычисляют среднее, группируют элементы или находят максимум. Если требовались два независимых результата, код часто выполнял стрим повторно либо вручную поддерживал несколько аккумуляторов.
В Java 12 появился teeing collector, который выразил такой сценарий как стандартную композицию коллекторов. Подход особенно полезен для неизменяемых или одноразовых источников, повторный обход которых невозможен или дорог.
Повторный запуск стрима для получения разных агрегатов невозможен: после терминальной операции стрим считается использованным. Создание двух стримов от коллекции обычно допустимо, но приводит к двум обходам источника и может увеличить задержку.
Ручное накопление нескольких значений в одном изменяемом объекте тоже имеет риски: нужно самостоятельно проектировать аккумулятор, объединение частичных результатов и корректную работу в параллельном режиме.
teeing принимает два downstream-коллектора и функцию объединения. Во время обхода каждый входной элемент передаётся аккумулятору первого и второго коллектора. В параллельном режиме частичные состояния каждого downstream-коллектора объединяются его собственным combiner.
После завершения накопления оба downstream-коллектора применяют свои finisher-функции. Затем merger получает два финальных результата и создаёт итоговое значение. Таким образом, merger работает не с внутренними аккумуляторами, а с результатами, объявленными типами downstream-коллекторов.
Здесь список обходится один раз: первый коллектор вычисляет среднее, второй — максимум. Результат второго коллектора имеет тип Optional<Integer>, поэтому пустой источник обрабатывается явно.
За один проход не означает бесплатную обработку: каждый элемент участвует в двух накоплениях, а промежуточное состояние может требовать суммарно больше памяти. Downstream-коллекторы должны иметь корректные supplier, accumulator и combiner; teeing не исправляет нарушения их контрактов.
Для параллельного стрима корректность также зависит от ассоциативности объединения частичных состояний downstream-коллекторов. Итоговый merger должен корректно работать с двумя финальными результатами, но ему не требуется быть combiner-ом для внутренних аккумуляторов.
Сервис формирует статистику большого набора чисел: требуется среднее значение и максимальный элемент. Первый вариант запускает два независимых стрима. Он прост для чтения, но дважды проходит источник и не подходит для одноразового источника вроде потока данных из файла.
Второй вариант вручную создаёт объект статистики и обновляет в нём сумму, количество и максимум. Он может быть эффективным, но смешивает несколько задач в одном аккумуляторе и усложняет параллельное объединение частичных результатов.
Выбран teeing с averagingInt и maxBy. Каждый элемент читается один раз, стандартные коллекторы отвечают за собственные инварианты, а merger только собирает финальный объект Stats. Компромисс — две операции накопления на элемент и необходимость учитывать Optional для пустого источника.
Нет. Источник обходится одним конвейером, а каждый элемент внутри этого обхода направляется в оба downstream-коллектора. Это отличается от последовательного запуска двух терминальных операций, где потребовались бы два отдельных стрима и два обхода источника.
Однако вычислительная работа двух downstream-коллекторов никуда не исчезает. Экономится прежде всего повторное чтение и прохождение источника, а не все затраты обработки.
Каждая параллельная часть получает собственные состояния обоих downstream-коллекторов. При объединении сначала применяются combiner первого коллектора и combiner второго, а после завершения накопления выполняются их finisher-функции и общий merger.
Поэтому downstream-коллекторы должны поддерживать корректное разделение работы и объединение частичных результатов. Если их combiner неассоциативен или аккумулятор небезопасен для предусмотренного режима, teeing не гарантирует корректный результат.
Да, но результат зависит от контракта этого коллектора. Например, maxBy возвращает Optional.empty() для пустого источника, а counting возвращает ноль.
Merger обязан учитывать такие значения. teeing не подменяет семантику downstream-коллекторов и не превращает отсутствие результата в произвольное значение.