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

Backend-разработчик Java
Учебный маршрут backend-разработчика на Java: язык и JVM, коллекции и многопоточность, Spring, реляционные данные и JPA, Kafka, безопасность, продакшн-инженерия и архитектура.

Backend Developer Java
Глубокое понимание Java и JVM для backend-разработки. Курс разбирает не синтаксис, а семантику языка и поведение платформы: коллекции и их стоимость, Stream API, устройство JVM — память, сборку мусора, JIT, — модель памяти и многопоточность, внутренности Spring (контейнер, прокси, транзакции), JPA/Hibernate, Kafka и production-практики: тестирование, наблюдаемость, контейнеры, устойчивость. Цель — научиться видеть стоимость абстракций и поведение кода под нагрузкой, а не просто заставлять его работать.
Готовьтесь не вслепую
Возьмите маршрут по своей роли и отрабатывайте ответы на вопросы с AI-разбором.