В чем разница между промежуточными и терминальными операциями в Stream API Java?

Ответ

В Java Stream API операции делятся на промежуточные (intermediate) и терминальные (terminal), что определяет их поведение в цепочке вызовов.

Промежуточные операции (например, filter(), map(), sorted(), distinct()):

  • Ленивые (lazy) — не выполняют обработку немедленно, а лишь добавляют новую операцию в конвейер.
  • Возвращают новый Stream, что позволяет строить цепочки (pipeline).
  • Обработка данных начинается только при вызове терминальной операции.

Терминальные операции (например, collect(), forEach(), reduce(), count(), anyMatch()):

  • Энергичные (eager) — запускают выполнение всего конвейера, обрабатывают элементы и производят результат или побочный эффект.
  • Возвращают не Stream, а конкретное значение (void, Optional, List, boolean и т.д.).
  • После вызова терминальной операции поток считается потребленным и не может быть использован повторно.

Пример:

List<String> result = names.stream()          // Источник потока
        .filter(name -> name.length() > 3)   // Промежуточная операция: фильтрация
        .map(String::toUpperCase)            // Промежуточная операция: преобразование
        .collect(Collectors.toList());       // Терминальная операция: сбор в список

В этом примере filter и map лишь настраивают конвейер. Фактический обход коллекции names, применение фильтра и маппера происходит внутри вызова collect().

Ответ 18+ 🔞

А, блядь, смотри, сейчас объясню на пальцах, как эта ваша Stream API работает, а то народ путается, как слепой кот в сортире. Тут всё просто, как три копейки, но если не понять разницу — будет тебе хиросима и нигерсраки в коде.

Представь себе водопроводную трубу, да? Это и есть стрим. Промежуточные операции — это когда ты к этой трубе прикручиваешь всякие штуки: фильтр от ржавчины (.filter()), нагреватель (.map()), или изгиб, чтобы вода крутилась (.sorted()). Ты их навесил, но вода-то ещё не потекла! Ты просто подготовил систему, ёпта. Они ленивые, как мой сосед в воскресенье — сами ничего не делают, только ждут команды. И каждая такая операция возвращает тебе новую, модифицированную трубу (новый Stream), чтобы ты мог дальше прикручивать.

А теперь терминальная операция — это когда ты, наконец, открываешь кран! .collect(), .forEach(), .count(). Вот тут-то всё и начинается: вода хлынула, прошла через все твои фильтры и нагреватели, и на выходе получился конкретный результат — налитая кружка (List), счётчик капель (long) или просто плеск в раковину (void).

И главное, чувак, после того как кран открыл — всё, труба пуста! Повторно из неё не попьёшь. Попробуешь — получишь IllegalStateException, вот это удивление пиздец будет. Поток одноразовый, как зажигалка с барахолки.

Смотри на примере, тут всё видно:

List<String> result = names.stream()          // Вот тебе голая труба с именами
        .filter(name -> name.length() > 3)   // Прикрутил сито: короткие имена — в мусор
        .map(String::toUpperCase)            // Поставил обогреватель: делает все буквы большими
        .collect(Collectors.toList());       // А вот и открыл кран! Собирай результат в ведро (List).

Пока не вызвал collect(), нихуя не происходит. Компьютер смотрит на эту цепочку и думает: «Э, сабака сука, опять эти ваши ленивые стримы собрали... ну ладно, подожду». И ждёт терминальной команды. Как только она пришла — ёперный театр! — запускается весь конвейер разом.

Запомни: lazy (ленивый) — значит «сделаю потом, когда спросите», eager (энергичный) — значит «щас всё сделаю, вот результат, доволен?». И да, не пытайся использовать поток дважды — он не для этого. Сделал дело — создавай новый.