← Назад к вопросам
Когда стрим начинает работу над коллекцией?
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)
- Оптимизировать производительность
- Работать со скользящим окном