Backend · Java

Stream API на собеседовании по Java

Промежуточные и терминальные операции называют все, а спрашивают про ленивость и побочные эффекты. Разбираем, что именно проверяет вопрос про Stream API.

Про Stream API чаще всего готовятся так: выучить, какие операции промежуточные, а какие терминальные. Список выучивается за вечер и почти ничего не даёт, потому что интервьюер, набиравший команду в 2024–2025, описывает свою секцию иначе: задачи на стримы — отдельный класс задач лайвкодинга, и код там пишут, а не пересказывают. Ниже — четыре свойства Stream API, которых в списках операций нет, а в документации они записаны прямо.

Всё, что сказано о поведении, относится к Java SE 21.

Ленивость — это контракт, а не оптимизация §

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

Отсюда сразу два следствия, которые проверяются одной строкой кода. Первое: конвейер без терминальной операции не делает ничего вообще — не «делает лениво», а буквально ничего, потому что обход не начинался. Второе: filter не фильтрует в момент вызова. Он возвращает новый стрим, который при обходе будет содержать подходящие элементы, — и это не придирка к формулировке, а причина, по которой следующая секция вообще возможна.

Из того же контракта следует и запрет на повторное использование. После терминальной операции конвейер считается использованным и больше применяться не может; если нужно пройти по тем же данным снова, возвращаются к источнику и берут новый стрим.

Почему побочный эффект может не выполниться §

Это самое неожиданное место темы, и оно записано в документации прямо. Если параметры операций имеют побочные эффекты, нет никаких гарантий — ни на видимость этих эффектов другим потокам, ни на то, что разные операции над одним элементом выполнятся в одном потоке, ни, главное, на то, что параметры вообще будут вызваны: реализация вправе исключить операции или целые стадии конвейера, если может доказать, что на результат вычисления это не повлияет.

Практический вывод формулируется коротко: print внутри map может не напечатать ничего, если результат map никому не нужен. Документация оговаривает исключение — терминальные forEach и forEachOrdered; для остальных случаев побочные эффекты «могут выполняться не всегда».

Рядом лежат два требования к параметрам операций, которые обычно называют вместе с этим:

  • Non-interference — параметр не должен изменять источник данных стрима, если источник не конкурентный.
  • Stateless — результат параметра не должен зависеть от состояния, которое может меняться во время выполнения конвейера; при нарушении результат может оказаться недетерминированным или попросту неверным.

Оба требования становятся понятнее из предыдущей секции: раз момент обхода не совпадает с моментом описания конвейера, любая опора на изменяющееся состояние — опора на неизвестный момент времени.

Где Collectors ведёт себя не так, как ожидают §

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

toMap падает на дубликатах ключей. Вариант с двумя аргументами бросает IllegalStateException, если сопоставленные ключи содержат дубликаты — совпадение определяется по Object.equals. Это не редкий крайний случай: любая группировка по неуникальному полю приводит к нему сразу. Документация называет и лечение — вариант toMap с третьим аргументом, функцией слияния, которая решает, что делать с двумя значениями одного ключа.

groupingBy не обещает, что вернёт. Формулировка документации не оставляет места толкованию: нет никаких гарантий на тип, изменяемость, сериализуемость и потокобезопасность возвращаемых Map и List. Отсюда практическое следствие: код, который берёт результат groupingBy и добавляет в него элемент, опирается на то, что не гарантировано, — и может перестать работать при смене версии.

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

Когда параллельность вредит §

Замена stream() на parallelStream() выглядит как бесплатное ускорение — одно слово, и обработка идёт на всех ядрах. Документация даёт конкретный документированный случай, где параллельность контрпродуктивна: сложная свёртка через collect(), строящая Map — например, группировка, — потому что стадия слияния, объединение одной карты с другой по ключам, для некоторых реализаций Map обходится дорого.

Механизм здесь простой и его стоит проговорить: параллельная обработка делит данные на части, обрабатывает их независимо и потом объединяет промежуточные результаты. Свёртка вроде суммы объединяется дёшево — сложить два числа. Свёртка, строящая карту, объединяется дорого — нужно слить два набора ключей. Выигрыш от параллельной обработки съедается стадией слияния.

Документация описывает и условия, при которых свёртка выполняется конкурентно, а не через слияние: стрим должен быть параллельным, сборщик — обладать характеристикой CONCURRENT, и при этом либо стрим неупорядочен, либо неупорядоченность объявлена у самого сборщика. Все три условия обязательны — это и объясняет, почему groupingByConcurrent существует отдельно от groupingBy.

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

Проверьте себя

Вопросы из разбора одним списком. Если на каждый есть ответ своими словами — тему можно считать закрытой.

  1. Почему Stream называют ленивым?
  2. Что напечатает конвейер, если поставить вывод внутрь map?
  3. Что произойдёт, если у двух элементов совпадут ключи в toMap?
  4. Ускорит ли parallelStream эту обработку?
По этой теме на платформе

Готовьтесь не вслепую

Возьмите маршрут по своей роли и отрабатывайте ответы на вопросы с AI-разбором.