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

Когда стрим начинает работу над коллекцией?

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

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

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

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

Когда стрим начинает работу над коллекцией?

Стримы в Java работают в два этапа: сначала строят цепочку операций (лениво), а потом выполняют (когда нужен результат).

Ключевой момент: Терминальные операции

Стрим начинает РЕАЛЬНУЮ работу только при вызове терминальной операции.

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

// ❌ ЭТА СТРОКА НЕ ДЕЛАЕТ НИЧЕГО!
// Только создаёт цепочку промежуточных операций
Stream<Integer> stream = numbers.stream()
    .filter(n -> n > 2)
    .map(n -> n * 2);
    // Никакого выполнения!

// ✅ ТОЛЬКО ЭТА СТРОКА запускает выполнение
List<Integer> result = stream.collect(Collectors.toList());  // Терминальная операция

Промежуточные vs Терминальные операции

Промежуточные операции (Intermediate):

  • filter() — условие
  • map() — преобразование
  • flatMap() — разворачивание
  • sorted() — сортировка
  • distinct() — уникальные
  • limit() — ограничение
  • skip() — пропуск
  • Возвращают новый Stream — не выполняются сразу

Терминальные операции (Terminal):

  • collect() — собрать результат
  • forEach() — обход
  • reduce() — свёртка
  • count() — подсчёт
  • findFirst(), findAny() — поиск
  • anyMatch(), allMatch(), noneMatch() — проверка
  • min(), max() — экстремумы
  • Возвращают результат (не Stream) — НАЧИНАЮТ выполнение

Пример: Ленивое вычисление

public class StreamLaziness {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        System.out.println("Начало");
        
        // ЭТА ЦЕПОЧКА НЕ ВЫПОЛНЯЕТСЯ
        Stream<Integer> stream = numbers.stream()
            .peek(n -> System.out.println("  filter проверяет: " + n))
            .filter(n -> n > 5)
            .peek(n -> System.out.println("  filter прошёл: " + n))
            .map(n -> {
                System.out.println("  map преобразует: " + n);
                return n * 2;
            });
        
        System.out.println("После создания стрима (ничего не напечаталось!)");
        
        // ТОЛЬКО ЗДЕСЬ начинается выполнение
        List<Integer> result = stream.collect(Collectors.toList());
        
        System.out.println("Результат: " + result);
    }
}

/* Вывод:
Начало
После создания стрима (ничего не напечаталось!)
  filter проверяет: 1
  filter проверяет: 2
  filter проверяет: 3
  filter проверяет: 4
  filter проверяет: 5
  filter проверяет: 6
  filter прошёл: 6
  map преобразует: 6
  filter проверяет: 7
  filter прошёл: 7
  map преобразует: 7
  ... и так далее
Результат: [12, 14, 16, 18, 20]
*/

Почему это важно: Оптимизация

Ленивость позволяет оптимизировать даже без явного указания:

List<String> names = Arrays.asList(
    "Alice", "Bob", "Charlie", "Diana", "Eve"
);

// Стрим УМНЫЙ и остановится после первого совпадения
String first = names.stream()
    .peek(n -> System.out.println("  проверяю: " + n))
    .filter(n -> n.startsWith("C"))
    .findFirst()  // Терминальная операция
    .get();

/* Вывод:
  проверяю: Alice
  проверяю: Bob
  проверяю: Charlie
Результат: Charlie

Обратите внимание: Diana и Eve не проверялись!
Стрим остановился, как только нашёл первое совпадение.
*/

Порядок выполнения: Вертикальный vs Горизонтальный

// ГОРИЗОНТАЛЬНЫЙ порядок (неправильное ожидание)
List<Integer> data = Arrays.asList(1, 2, 3, 4, 5);

// Вы думаете:
// 1. Все фильтруются (1, 2, 3, 4, 5)
// 2. Все маппируются
// 3. Все печатаются

// На деле (ВЕРТИКАЛЬНЫЙ порядок):
// Для каждого элемента:
//   1. фильтр
//   2. map
//   3. печать

data.stream()
    .filter(n -> n > 2)     // Промежуточная
    .map(n -> n * 2)        // Промежуточная
    .forEach(n -> System.out.println(n));  // Терминальная - ВЫПОЛНЕНИЕ

/* Реальный порядок:
1. Берём элемент 1 → filter(1 > 2? нет) → пропускаем
2. Берём элемент 2 → filter(2 > 2? нет) → пропускаем
3. Берём элемент 3 → filter(3 > 2? да) → map(3*2=6) → println(6)
4. Берём элемент 4 → filter(4 > 2? да) → map(4*2=8) → println(8)
5. Берём элемент 5 → filter(5 > 2? да) → map(5*2=10) → println(10)
*/

Ошибки, которые делают начинающие

Ошибка 1: Забыли терминальную операцию

// ❌ Ничего не произойдёт
List<String> data = Arrays.asList("a", "b", "c");
data.stream()
    .map(String::toUpperCase);
// Стрим создан, но не выполнен!

// ✅ Правильно
data.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println);  // Вот здесь выполнится

Ошибка 2: Переиспользование стрима

// ❌ Стрим можно использовать только один раз
Stream<Integer> stream = numbers.stream();
stream.filter(n -> n > 5).forEach(System.out::println);
stream.map(n -> n * 2).forEach(System.out::println);  // IllegalStateException!

// ✅ Каждый раз новый стрим
numbers.stream().filter(n -> n > 5).forEach(System.out::println);
numbers.stream().map(n -> n * 2).forEach(System.out::println);

Производительность: Когда остановиться

List<Integer> millionNumbers = /* 1 млн чисел */;

// ✅ БЫСТРО — стрим остановится на первом совпадении
boolean hasEven = millionNumbers.stream()
    .anyMatch(n -> n % 2 == 0);  // Найдёт первое чётное и остановится

// ❌ МЕДЛЕННО — перебирает ВСЕ элементы
int count = millionNumbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList())
    .size();  // Создал список, а потом размер?

// ✅ ПРАВИЛЬНО
long count = millionNumbers.stream()
    .filter(n -> n % 2 == 0)
    .count();  // Считает прямо в стриме

Итог

Стрим начинает работу над коллекцией:

  • ❌ НЕ когда вызываете stream()
  • ❌ НЕ когда вызываете промежуточные операции (filter, map, ...)
  • ТОЛЬКО когда вызываете терминальную операцию (collect, forEach, count, ...)

Это называется "ленивое вычисление" (Lazy Evaluation) и позволяет:

  • Избежать лишних операций
  • Остановиться на первом результате (findFirst)
  • Оптимизировать производительность
  • Работать со скользящим окном
Когда стрим начинает работу над коллекцией? | PrepBro