Ленивость стримов
В прошлом уроке конвейер заканчивался сборщиком toList(). Давайте уберём сборщик и посмотрим, что сделает лента. Карточка для станции filter будет печатать каждое слово, прежде чем его проверить:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> words = List.of("cat", "ox");
words.stream().filter(w -> {
System.out.println("check " + w);
return w.length() > 2;
});
System.out.println("finished"); // finished
}
}Программа печатает только finished. Строк check cat и check ox нет: лямбда в filter не выполнилась ни разу.
Без сборщика лента стоит
В этом главное отличие стрима от цикла. Цикл for выполняет тело сразу. А filter и map с элементами ничего не делают: они только вручают станциям карточки и возвращают новый стрим.
Лента трогается, только когда в конце встаёт сборщик — терминальная операция. Промежуточные операции, такие как filter и map, называют ленивыми: работа откладывается до момента, когда понадобится результат. Нет терминальной операции — нет и работы.
Что выполнится в стриме без терминальной операции — частый вопрос на собеседовании. Ответ: ничего.
Элементы едут по одному
Хорошо, сборщик на месте. В каком порядке тогда работают станции? Можно подумать, что сначала filter проверяет все элементы, а потом map обрабатывает всех, кто прошёл. На деле каждый элемент проезжает всю ленту, и только потом на неё встаёт следующий.
В виджете две карточки: isLong для станции filter и up для станции map, и каждая печатает, что делает. Рамка isLong.test — это тело лямбды isLong, которое стрим вызывает через метод test, а рамка up.apply — тело лямбды up. Между Main.main и этими рамками на самом деле работают методы самого стрима, но виджет их не показывает. Нажимайте «Следующий шаг» и следите за порядком строк в выводе.
1import java.util.List;2import java.util.function.Function;3import java.util.function.Predicate;4 5public class Main {6 public static void main(String[] args) {7 Predicate<String> isLong = w -> {8 System.out.println("test " + w);9 return w.length() > 2;10 };11 Function<String, String> up = w -> {12 System.out.println("map " + w);13 return w.toUpperCase();14 };15 var s = List.of("cat", "ox", "emu")16 .stream()17 .filter(isLong)18 .map(up);19 System.out.println("built");20 System.out.println(s.toList());21 }22}
isLong — карточка для станции filter: тело лямбды сейчас не выполняется.
Посмотрите на вывод. built напечаталось раньше всего, хотя filter и map стоят в программе выше. cat проехал обе станции до того, как ox встал на ленту, а ox до map не доехал вовсе.
Такой порядок даёт стриму две выгоды. Ему не нужны промежуточные списки: каждый элемент сразу едет дальше. А операция, которой хватает первого подходящего элемента, может остановить ленту и не трогать остальные.
Здесь картинка с конвейером неточна. По настоящей ленте едет сразу много коробок, одна за другой. В стриме на ленте в каждый момент только один элемент: следующий встаёт на неё, когда предыдущий доехал до конца или сошёл с ленты.
Частая ошибка
Стрим сохранили в переменную и запустили дважды:
var s = words.stream().filter(w -> w.length() > 2);
System.out.println(s.count()); // 1
System.out.println(s.toList()); // ✗Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closedПервая печать выводит 1, а на второй терминальной операции программа падает. Стрим одноразовый: после сборщика лента разобрана, и второй раз её не запустить. Если нужен ещё один результат, постройте новый стрим: words.stream() можно вызывать сколько угодно раз.
Главное
- Промежуточные операции только строят конвейер: без терминальной операции ни одна лямбда не выполнится.
- Каждый элемент проходит все станции подряд, и только потом стрим берёт следующий.
- Элемент, который не прошёл
filter, до следующих станций не доезжает. - Стрим одноразовый: вторая терминальная операция бросает
IllegalStateException.