В параллельном сборе Stream с нетривиальным Collector когда применяется finisher и почему его нельзя считать обработчиком каждого частичного результата?
В параллельном сборе finisher применяется к итоговому промежуточному контейнеру после завершения накопления и объединения частичных результатов. Он не предназначен для обработки каждого контейнера, созданного отдельной ветвью вычисления. Если у Collector есть характеристика IDENTITY_FINISH, реализация может вообще не вызывать finisher, поскольку промежуточный контейнер уже является итоговым результатом.
Stream API появился в Java 8, чтобы отделить описание обработки данных от способа её выполнения. Для параллельной обработки потребовалась модель, в которой элементы можно накапливать независимо, а затем объединять частичные результаты.
Поэтому Collector разделяет несколько этапов: создание контейнера, добавление элементов, объединение контейнеров и преобразование накопленного результата. Финальное преобразование было вынесено в finisher, чтобы тип рабочего контейнера мог отличаться от типа результата.
В параллельном стриме поток элементов обычно разбивается на несколько частей. Каждая часть может обрабатываться в своём контейнере, после чего контейнеры объединяются в дерево операций объединения.
Если ошибочно воспринимать finisher как обработчик каждой части, можно выполнить дорогое преобразование много раз, получить некорректный результат или разместить в нём логику, зависящую от порядка обработки. Особенно опасно использовать finisher для побочных эффектов: контракт Collector предназначен для формирования результата, а не для гарантированного управления количеством таких эффектов.
Для обычного Collector схема выглядит так:
В последовательной обработке обычно создаётся один контейнер. В параллельной обработке контейнеров может быть много, но finisher применяется к результату финального объединения, а не к каждому из них.
Например, finisher может превратить изменяемый список накопления в неизменяемый. При этом сами частичные списки остаются внутренней деталью сбора:
Здесь finisher концептуально работает с окончательным списком после combiner. Для Collector без IDENTITY_FINISH ожидается одно финальное преобразование результата. Но код не должен полагаться на точное число вызовов функций Collector для побочных эффектов: такие функции должны быть корректными в рамках контракта сбора и не зависеть от наблюдаемого порядка выполнения.
Характеристика IDENTITY_FINISH означает, что тип промежуточного контейнера совпадает с типом результата и финальное преобразование является тождественным. В таком случае реализация имеет право вернуть контейнер напрямую и не вызывать finisher. Поэтому побочные эффекты внутри finisher недопустимы как механизм гарантированного выполнения.
Для параллельного сбора также важны ассоциативность combiner, потокобезопасность при использовании характеристики CONCURRENT и отсутствие зависимости от конкретного числа частичных контейнеров. Если эти условия нарушены, результат может быть неверным независимо от корректности finisher.
Сервис собирает результаты параллельного поиска в изменяемый рабочий список, а наружу должен возвращать неизменяемый список.
Первый вариант — сделать каждый частичный контейнер сразу неизменяемым. Это неудобно: combiner должен будет создавать новые контейнеры при каждом объединении, что увеличит количество копирований и потребление памяти.
Второй вариант — использовать изменяемые контейнеры во время накопления, объединять их через combiner, а в finisher один раз вызвать преобразование в неизменяемый список. Этот подход разделяет внутреннее накопление и внешний контракт результата.
Выбран второй вариант: изменяемость ограничена внутренним этапом Collector, а финальный результат формируется после завершения объединения. Это уменьшает число копирований и не требует, чтобы промежуточные контейнеры были безопасны для конкурентной записи, если Collector не объявлен CONCURRENT.
1. Может ли finisher изменить порядок элементов в параллельном сборе?
Да, если сам Collector допускает такой результат или finisher явно выполняет переупорядочивание. Но finisher не восстанавливает автоматически порядок, нарушенный некорректным combiner или использованием неупорядоченного сбора. Гарантии порядка определяются источником стрима, Collector и логикой всех этапов сбора.
2. Почему finisher не должен изменять входной контейнер неожиданным образом?
Finisher получает итоговый промежуточный результат и преобразует его в конечный. Изменение контейнера допустимо только если оно совместимо с контрактом Collector и не приводит к неожиданным наблюдаемым эффектам. Обычно безопаснее вернуть новый результат, например неизменяемую копию, особенно если рабочий контейнер должен оставаться внутренней деталью реализации.
3. Можно ли использовать число вызовов finisher для определения степени параллелизма?
Нет. Число частичных контейнеров и внутренняя стратегия выполнения являются деталями реализации. В обычном Collector finisher относится к финальному результату, а характеристика IDENTITY_FINISH позволяет его пропустить. Поэтому измерять параллелизм через побочные эффекты Collector нельзя; для этого применяют профилирование и явные метрики выполнения.