← Назад к вопросам

В какой момент выполняются промежуточные методы в Stream

2.0 Middle🔥 81 комментариев
#Stream API и функциональное программирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# В какой момент выполняются промежуточные методы в Stream?

Краткий ответ

Промежуточные методы выполняются ленивым образом — только когда вызывается терминальная операция. До этого они просто описывают преобразования.

Что такое промежуточные методы?

Промежуточные (intermediate) методы — это методы, которые возвращают новый Stream:

// Все эти методы - промежуточные
stream
    .filter(x -> x > 5)           // промежуточный
    .map(x -> x * 2)               // промежуточный
    .distinct()                    // промежуточный
    .limit(10)                     // промежуточный
    .skip(2)                       // промежуточный
    .flatMap(...)                  // промежуточный
    .sorted()                      // промежуточный

Терминальные операции

Терминальные (terminal) методы — они не возвращают Stream и запускают вычисления:

    .forEach(System.out::println)      // терминальная
    .collect(Collectors.toList())      // терминальная
    .reduce((a, b) -> a + b)           // терминальная
    .findFirst()                       // терминальная
    .count()                           // терминальная
    .anyMatch(x -> x > 0)              // терминальная

Ленивые вычисления на примере

Пример 1: Промежуточные методы без терминального

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// Этот код НЕ выполнит ничего!
stream
    .filter(x -> {
        System.out.println("filter: " + x);
        return x > 2;
    })
    .map(x -> {
        System.out.println("map: " + x);
        return x * 2;
    });

// Консоль пуста!
// Промежуточные методы не вызваны

Пример 2: С терминальной операцией

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

List<Integer> result = numbers.stream()
    .filter(x -> {
        System.out.println("filter: " + x);
        return x > 2;
    })
    .map(x -> {
        System.out.println("map: " + x);
        return x * 2;
    })
    .collect(Collectors.toList());  // ТЕРМИНАЛЬНАЯ ОПЕРАЦИЯ!

// Вывод:
// filter: 1
// filter: 2
// filter: 3
// map: 3
// filter: 4
// map: 4
// filter: 5
// map: 5

System.out.println(result);  // [6, 8, 10]

Порядок выполнения: Вертикальное или горизонтальное?

Неправильное понимание (горизонтальное)

Много людей ошибочно думают, что операции выполняются в таком порядке:

Шаг 1: filter все элементы → [3, 4, 5]
Шаг 2: map все элементы → [6, 8, 10]
Шаг 3: collect результат → [6, 8, 10]

Правильное понимание (вертикальное)

На самом деле каждый элемент проходит весь pipeline полностью:

Элемент 1:
  1 → filter(1 > 2?) → НЕТ → пропущен

Элемент 2:
  2 → filter(2 > 2?) → НЕТ → пропущен

Элемент 3:
  3 → filter(3 > 2?) → ДА → map(3 * 2 = 6) → 6

Элемент 4:
  4 → filter(4 > 2?) → ДА → map(4 * 2 = 8) → 8

Элемент 5:
  5 → filter(5 > 2?) → ДА → map(5 * 2 = 10) → 10

Практические примеры

Пример с limit и skip

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.stream()
    .peek(x -> System.out.println("peek1: " + x))
    .filter(x -> x > 2)
    .peek(x -> System.out.println("peek2: " + x))
    .skip(1)
    .peek(x -> System.out.println("peek3: " + x))
    .limit(2)
    .peek(x -> System.out.println("peek4: " + x))
    .forEach(x -> System.out.println("result: " + x));

Вывод:

peek1: 1
peek1: 2
peek1: 3
peek2: 3      (прошёл filter)
peek1: 4
peek2: 4
peek3: 4      (после skip)
peek1: 5
peek2: 5
peek3: 5      (после skip)
peek4: 5      (в limit)
result: 5
peek1: 6
peek2: 6
peek3: 6
peek4: 6      (в limit)
result: 6     (limit=2, получили 2 элемента, стоп)

Важная оптимизация: short-circuit операции

Некоторые операции требуют обработки всех элементов, другие — нет:

// Требует обработки ВСЕХnumbers
List<Integer> result = numbers.stream()
    .filter(x -> x > 2)
    .collect(Collectors.toList());  // Обработает все элементы

// Требует ВСЕ элементы для подсчёта
long count = numbers.stream()
    .filter(x -> x > 2)
    .count();  // Обработает все элементы

// НЕ требует обработку ВСЕх элементов (short-circuit)
boolean any = numbers.stream()
    .filter(x -> x > 2)
    .anyMatch(x -> x == 5);  // Может остановиться на первом совпадении

// НЕ требует все элементы
Optional<Integer> first = numbers.stream()
    .filter(x -> x > 2)
    .findFirst();  // Остановится на первом найденном элементе

flatMap - особый случай

List<List<Integer>> matrix = Arrays.asList(
    Arrays.asList(1, 2, 3),
    Arrays.asList(4, 5, 6),
    Arrays.asList(7, 8, 9)
);

matrix.stream()
    .peek(list -> System.out.println("peek1: " + list))
    .flatMap(list -> {
        System.out.println("flatMap: " + list);
        return list.stream();
    })
    .peek(x -> System.out.println("peek2: " + x))
    .collect(Collectors.toList());

flatMap создаёт новый stream для каждого элемента исходного потока.

Когда промежуточные методы выполняются?

СитуацияВыполнение
Есть терминальная операция✅ Выполняются
Нет терминальной операции❌ НЕ выполняются
Есть stream но не сохранён❌ НЕ выполняются
Сохранён как переменная❌ НЕ выполняются (пока не вызвать terminal)

Типы промежуточных операций

Stateless (без состояния)

Выполняют одинаково для каждого элемента:

  • filter()
  • map()
  • flatMap()
  • peek()

Stateful (с состоянием)

Нужно знать информацию о других элементах:

  • distinct() — помнит все предыдущие элементы
  • sorted() — должна знать все элементы перед сортировкой
  • limit() — считает количество элементов
  • skip() — пропускает первые N элементов
// distinct требует буфера для отслеживания уникальности
numbers.stream()
    .distinct()  // stateful - должна помнить все элементы
    .forEach(System.out::println);

// sorted требует всех элементов ДО сортировки
numbers.stream()
    .sorted()  // stateful - должна прочитать все перед выводом
    .forEach(System.out::println);

Вывод

  1. Промежуточные методы ленивы — выполняются только с терминальной операцией
  2. Вертикальное выполнение — каждый элемент проходит весь pipeline
  3. Short-circuit операции — findFirst(), anyMatch() и т.д. могут остановиться раньше
  4. Stateful операции — distinct(), sorted() требуют буфера
  5. Без terminal операции — ничего не происходит (даже side-effects в peek())
В какой момент выполняются промежуточные методы в Stream | PrepBro