Stream API
Конвейеры Stream — промежуточные и терминальные операции, ленивость, map против flatMap, коллекторы, reduce против collect, параллельные стримы и gatherers.
12 вопросов
JuniorТеорияОчень частоЧто такое Stream и чем он отличается от цикла for по коллекции?
Что такое Stream и чем он отличается от цикла for по коллекции?
Stream — это конвейер над последовательностью элементов, а не контейнер, который их хранит. У него есть источник (коллекция или массив), промежуточные операции вроде filter и map и одна терминальная операция вроде collect. В отличие от цикла он описывает, что вычислить, а не как итерировать, и он никогда не меняет свой источник — он даёт новые результаты.
Типичные ошибки
- ✗Думать, что Stream хранит свои элементы так же, как
List - ✗Считать, что
filterилиmapизменяют исходную коллекцию - ✗Строить цепочку промежуточных операций и ждать работы без терминальной операции
Уточняющие вопросы
- →Какие части конвейера stream отложенные и что на самом деле запускает работу?
- →Почему один и тот же экземпляр stream нельзя обойти дважды?
JuniorТеорияОчень частоЧем промежуточные операции stream отличаются от терминальных?
Чем промежуточные операции stream отличаются от терминальных?
Промежуточная операция вроде filter или map возвращает другой Stream, поэтому она лишь добавляет стадию в конвейер. Терминальная операция вроде collect или count возвращает не-stream результат и именно она реально запускает конвейер. До этого терминального вызова не вычисляется ничего — промежуточные стадии отложены по замыслу.
Типичные ошибки
- ✗Ждать, что
filterилиmapвыполнятся в момент вызова - ✗Строить цепочку промежуточных операций и не вызывать терминальную
- ✗Полагать, что терминальная операция вернёт
Stream, который можно продолжать
Уточняющие вопросы
- →Почему stream только из промежуточных операций вообще ничего не выводит?
- →Как отложенность позволяет короткозамыкающей терминальной операции пропускать элементы?
MiddleКодЧастоНайдите ошибку в этой цепочке Optional
Найдите ошибку в этой цепочке Optional
Это ошибка компиляции, а не рантайма. map(authService::findClient) оборачивает уже-Optional результат, давая Optional<Optional<Client>>. Поэтому в следующем map client — это Optional<Client>, а не Client, и передача его в createResponse(Client) не проходит проверку типов. Исправьте использованием flatMap для findClient: flatMap разворачивает один уровень, давая Optional<Client>, поэтому следующий map получает настоящий Client.
Типичные ошибки
- ✗Называть ошибку рантаймовой, хотя она ловится при компиляции
- ✗Упускать, что
mapнад функцией, возвращающейOptional, вкладывает вOptional<Optional<T>> - ✗Хвататься за
.get()вместоflatMapдля разворачивания одного уровня
Уточняющие вопросы
- →Когда использовать
map, а когдаflatMapнаOptional? - →Почему
Optional<Optional<T>>почти всегда признак проблемы в коде?
MiddleТеорияЧастоКак работает parallelStream и когда он ломается?
Как работает parallelStream и когда он ломается?
parallelStream разбивает источник и выполняет стадии в общем ForkJoinPool (по числу ядер), затем объединяет частичные результаты. Он ускоряет CPU-bound работу на больших разбиваемых источниках, но вредит малым или I/O-bound. Он молча ломает корректность, когда reduce использует не-нейтральный seed или неассоциативный аккумулятор либо когда лямбда меняет общее состояние — каждый поток заново применяет seed, а порядок теряется.
Типичные ошибки
- ✗Считать, что
parallelStreamвсегда ускоряет, в том числе на малых или I/O-bound задачах - ✗Использовать не-нейтральный seed или неассоциативный аккумулятор в параллельном
reduce - ✗Менять общее состояние из лямбды stream и ждать детерминированного результата
Уточняющие вопросы
- →Почему identity в
reduceобязан быть истинным нейтральным элементом при распараллеливании? - →Почему общий
ForkJoinPoolделает блокирующую задачу в параллельном stream опасной?
MiddleТеорияЧастоЧто такое конвейер Stream API и когда он реально выполняется?
Что такое конвейер Stream API и когда он реально выполняется?
Stream API обрабатывает последовательность элементов декларативно как конвейер: источник, промежуточные операции вроде filter и map и терминальную операцию вроде collect. Промежуточные операции отложенные — они строят конвейер, не итерируя; выполнение запускает только терминальная операция, прогоняя через него каждый элемент. Stream может также работать параллельно по потокам.
Типичные ошибки
- ✗Считать, что промежуточные операции выполняются сразу, а не строят отложенный конвейер
- ✗Переиспользовать stream после терминальной операции, что бросает
IllegalStateException - ✗Думать, что операции stream изменяют исходную коллекцию, а не дают новые результаты
Уточняющие вопросы
- →Как короткозамыкающая терминальная операция вроде
findFirstостанавливается раньше? - →Какой пул потоков обслуживает параллельный stream и когда параллелизм оправдан?
MiddleКодЧастоПодсчёт заказов по статусу через Collectors.groupingBy
Подсчёт заказов по статусу через Collectors.groupingBy
groupingBy(classifier) применяет классификатор к каждому элементу и собирает элементы в Map<K, List<T>> с ключом по его результату. Двухаргументный groupingBy(classifier, downstream) пропускает каждую корзину через второй коллектор вместо списка элементов, поэтому groupingBy(Order::status, counting()) даёт Map<Status, Long> со счётчиками за один проход: orders.stream().collect(groupingBy(Order::status, counting())).
Типичные ошибки
- ✗Ждать обычный
Map<K, List<T>>и затем считать циклом вместо передачиcounting() - ✗Думать, что
groupingByсортирует ключи или требуетComparable-ключ, какTreeMap - ✗Считать, что downstream-коллектор меняет ключи, а не сворачивает каждую корзину значений
Уточняющие вопросы
- →Чем
groupingBy(classifier, mapping(...))отличается отgroupingBy(classifier, counting())? - →Когда стоит передать поставщик map третьим аргументом в
groupingBy?
MiddleТеорияЧастоЧем reduce отличается от collect и когда выбирать каждый?
Чем reduce отличается от collect и когда выбирать каждый?
reduce — неизменяемая свёртка: он применяет BinaryOperator к identity и текущему значению, порождая на каждом шаге новое значение. collect — изменяемая свёртка: supplier создаёт контейнер, аккумулятор вкладывает каждый элемент, а комбайнер сливает два контейнера. Берите reduce для значения вроде суммы или максимума; берите collect, когда результат — контейнер, ведь свёртка String на каждый элемент выделяет O(n) мусора.
Типичные ошибки
- ✗Сворачивать
Stringчерезreduceи платить O(n) копий вместо сбора вStringBuilder - ✗Передавать в
reduceнеассоциативный аккумулятор или не-нейтральный identity, что ломается при параллелизме - ✗Считать, что
collectтребует внешней синхронизации, хотя каждый поток получает свой контейнер, сливаемый комбайнером
Уточняющие вопросы
- →Почему identity, переданный в
reduce, обязан быть истинным нейтральным элементом, когда stream идёт параллельно? - →Какие три функции нужны изменяемой свёртке и в чём задача комбайнера?
SeniorКодИногдаНапишите свой Collector, оставляющий n наибольших элементов
Напишите свой Collector, оставляющий n наибольших элементов
Строит его Collector.of(supplier, accumulator, combiner, finisher). Supplier создаёт свежий изменяемый контейнер на воркера, аккумулятор вкладывает один элемент, комбайнер сливает два контейнера — это и делает коллектор безопасным в параллели, — а finisher превращает контейнер в результат. Для topN контейнер — min-heap: кладём элемент, выбрасываем наименьший, как только их больше n, и сортируем уцелевших в finisher. Память остаётся O(n).
Типичные ошибки
- ✗Писать комбайнер, теряющий частичный результат одной из сторон, из-за чего параллельный ответ расходится с последовательным
- ✗Буферизовать все элементы и сортировать в finisher, из-за чего память становится O(входа), а не O(n)
- ✗Возвращать сам изменяемый контейнер вместо его преобразования в finisher
Уточняющие вопросы
- →Что обещает характеристика
UNORDEREDи когда её безопасно объявлять? - →Когда применима
IDENTITY_FINISHи что она экономит конвейеру?
SeniorПроизводительностьИногдаКак короткозамыкающие терминалы вроде findFirst и anyMatch ограничивают работу конвейера?
Как короткозамыкающие терминалы вроде findFirst и anyMatch ограничивают работу конвейера?
Отложенность тянет элементы по одному, поэтому короткозамыкающий терминал — findFirst, anyMatch, allMatch — прекращает тянуть, как только ответ решён: filter(p).findFirst() на миллионе элементов проверяет p лишь до первого совпадения. Это же делает бесконечный Stream.iterate пригодным под limit. Загвоздка в порядке: на параллельном упорядоченном stream findFirst обязан вернуть первое по порядку появления совпадение, в отличие от findAny.
Типичные ошибки
- ✗Считать, что каждая стадия материализует полную промежуточную коллекцию до запуска следующей
- ✗Ждать, что
findFirstстоит столько же, сколькоfindAny, на параллельном упорядоченном stream - ✗Думать, что бесконечный stream вообще нельзя обработать, а не ограничить короткозамыкающей стадией
Уточняющие вопросы
- →Почему
allMatchможет вернутьfalse, так и не посмотрев на оставшиеся элементы? - →Когда
findAnyзаметно дешевлеfindFirstи какой гарантией вы за это платите?
SeniorТеорияИногдаПочему переиспользование stream после терминальной операции бросает IllegalStateException?
Почему переиспользование stream после терминальной операции бросает IllegalStateException?
Stream — не контейнер: он держит spliterator источника и стадии конвейера и обходит источник ровно один раз. Конвейер несёт флаг «связан или израсходован», который выставляет терминальная операция, поэтому любая следующая операция бросает IllegalStateException. Ничего не буферизовано для повтора — spliterator не перемотать. Чтобы обойти дважды, соберите stream заново из источника или держите Supplier<Stream<T>>.
Типичные ошибки
- ✗Хранить
Streamв поле или переменной и отдавать его двум терминальным операциям - ✗Думать, что отложенность означает буферизацию элементов, которые можно проиграть заново
- ✗Ждать исключения только когда источник — файл или сокет, но не
List
Уточняющие вопросы
- →Как
Supplier<Stream<T>>позволяет вызывающему обойти один и тот же конвейер дважды? - →Почему добавление второй промежуточной операции к уже израсходованному stream тоже падает?
SeniorДебаггингИногдаПочему этот Collectors.toMap бросает IllegalStateException и как это исправить?
Почему этот Collectors.toMap бросает IllegalStateException и как это исправить?
Двухаргументный toMap(keyMapper, valueMapper) не знает правила для двух элементов, попавших на один ключ, поэтому второй сотрудник из Sales роняет его. Возьмите трёхаргументную перегрузку и дайте ей функцию слияния — toMap(Employee::department, Employee::salary, Integer::sum) — она сворачивает столкнувшиеся значения вместо падения. Эквивалент — groupingBy(Employee::department, summingInt(...)).
Типичные ошибки
- ✗Считать, что
toMapперезаписывает дублирующийся ключ, какMap.put, а не бросает исключение - ✗Хвататься за
distinct()или предварительный фильтр вместо того, чтобы объяснить коллектору, как сливать столкнувшиеся значения - ✗Забывать, что
toMapтакже бросаетNullPointerException, когда valueMapper возвращаетnull
Уточняющие вопросы
- →Когда стоит предпочесть
groupingByс downstream-коллектором трёхаргументномуtoMap? - →Как заставить
toMapвернутьTreeMapилиLinkedHashMapвместоHashMap?
MiddleТеорияРедкоЧто такое Gatherer и что он позволяет из того, чего не может Collector?
Что такое Gatherer и что он позволяет из того, чего не может Collector?
Gatherer (финализирован в Java 24, JEP 485) — своя промежуточная операция, аналог Collector для середины конвейера, подключаемый через stream.gather(g). Он собран из initializer, integrator, необязательного combiner и finisher, поэтому умеет хранить состояние между элементами, выдавать ноль, один или много выходных элементов на один входной и коротко замыкать источник. Collector же сворачивает лишь один раз — на терминальном шаге.
Типичные ошибки
- ✗Принимать
Gathererза терминальную операцию вродеCollector, а не за промежуточную стадию - ✗Считать, что промежуточная стадия обязана выдавать ровно один элемент на входной, поэтому окна требуют сперва
collect - ✗Упускать, что integrator может вернуть
falseи коротко замкнуть источник
Уточняющие вопросы
- →Какие готовые gatherers содержит
java.util.stream.Gatherersи что делаетscan? - →Как combiner у
Gathererделает стадию с состоянием пригодной для параллельного stream?