Теперь, когда мы узнали, как создавать разные типы потоков и работать с ними, давайте глубже погрузимся в процесс обработки потоковых операций.

Важная характеристика промежуточных операций - лень. Если мы выполним код без foreach, он ничего не напечатает. Это потому, что промежуточные операции будут выполняться только при наличии терминальной операции.
Stream.of("a2", "a1", "b1", "b3", "c2")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s));
//filter: a2
//forEach: a2
//filter: a1
//forEach: a1
//filter: b1
//forEach: b1
//filter: b3
//forEach: b3
//filter: c2
//forEach: c2
Возможен неожиданный порядок результатов. Наивным подходом было бы выполнять операции горизонтально, одну за другой, над всеми элементами потока. Но вместо этого по вертикали каждый элемент движется по цепочке. Первая строка «a2» проходит filter, затем forEach, только после этого обрабатывается вторая строка «a1».
Почему важен порядок исполнения
Очень важен порядок используемых промежуточных операций. Изменяя порядок операций, можно уменьшить количество выполнений. Давайте еще раз посмотрим, как выполняются эти операции:
Stream.of("a2", "a1", "b1", "b3", "c2")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));
//map: a2
//filter: A2
//forEach: A2
//map: a1
//filter: A1
//forEach: A1
//map: b1
//filter: B1
//map: b3
//filter: B3
//map: c2
//filter: C2
Как вы могли догадаться, и map, и filter вызываются пять раз для каждой строки в базовой коллекции, тогда как forEach вызывается только один раз.
Фактическое количество выполнений может быть значительно уменьшено, если мы изменим порядок операций, переместив filter в начало потока.
Stream.of("a2", "a1", "b1", "b3", "c2")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
//filter: a2
//map: a2
//forEach: A2
//filter: a1
//map: a1
//forEach: A1
//filter: b1
//filter: b3
//filter: c2
Теперь map вызывается только один раз, поэтому конвейер операций для больших объемов элементов потока выполняется намного быстрее. Имейте это в виду, составляя сложную операцию.
Давайте посмотрим на еще одну сложную промежуточную операцию: sorted
Stream.of("a2", "a1", "b1", "b3", "c2")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
//sort: a1; a2
//sort: b1; a1
//sort: b1; a2
//sort: b3; a2
//sort: b3; b1
//sort: c2; b1
//sort: c2; b3
//filter: a1
//map: a1
//forEach: A1
//filter: a2
//map: a2
//forEach: A2
//filter: b1
//filter: b3
//filter: c2
Операция сортировки - это так называемая операция с отслеживанием состояния, потому что вы должны поддерживать состояние для сортировки набора элементов. Сначала операция сортировки выполняется для всего набора входных данных.
И снова мы можем оптимизировать производительность, переупорядочив поток:
Stream.of("a2", "a1", "b1", "b3", "c2")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
//filter: a2
//filter: a1
//filter: b1
//filter: b3
//filter: c2
//sort: a1; a2
//map: a1
//forEach: A1
//map: a2
//forEach: A2
Как видите, операции сортировки выполняются меньше. Таким образом, производительность значительно увеличивается для больших входных коллекций.
В следующей статье я расскажу о расширенных потоковых операциях.