既然我们已经了解了如何创建并使用不同种类的数据流,让我们深入了解数据流操作在背后如何执行吧。衔接操作的一个重要特性就是延迟性。观察下面没有终止操作的例子:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
});
执行这段代码时,不向控制台打印任何东西。这是因为衔接操作只在终止操作调用时被执行。让我们通过添加终止操作forEach
来扩展这个例子:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
}).forEach(s -> System.out.println("forEach: " + s));
执行这段代码会得到如下输出:
filter: d2
forEach: d2
filter: a2
forEach: a2
filter: b1
forEach: b1
filter: b3
forEach: b3
filter: c
forEach: c
结果的顺序可能出人意料。原始的方法会在数据流的所有元素上,一个接一个地水平执行所有操作。但是每个元素在调用链上垂直移动。第一个字符串"d2"
首先经过filter
然后是forEach
,执行完后才开始处理第二个字符串"a2"
。
这种行为可以减少每个元素上所执行的实际操作数量,就像我们在下个例子中看到的那样:
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
}).anyMatch(s -> {
System.out.println("anyMatch: " + s);
return s.startsWith("A");
});
// map: d2
// anyMatch: D2
// map: a2
// anyMatch: A2
只要提供的数据元素满足了谓词,anyMatch
操作就会返回true
。对于第二个传递"A2"
的元素,它的结果为真。由于数据流的链式调用是垂直执行的,map
这里只需要执行两次。所以map
会执行尽可能少的次数,而不是把所有元素都映射一遍。
为什么顺序如此重要
下面的例子由两个衔接操作map
和filter
,以及一个终止操作forEach
组成。让我们再来看看这些操作如何执行:
Stream.of("d2", "a2", "b1", "b3", "c")
.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: d2
// filter: D2
// map: a2
// filter: A2
// forEach: A2
// map: b1
// filter: B1
// map: b3
// filter: B3
// map: c
// filter: C
就像你可能猜到的那样,map
和filter
会对底层集合的每个字符串调用五次,而forEach
只会调用一次。如果我们调整操作顺序,将filter
移动到调用链的顶端,就可以极大减少操作的执行次数:
Stream.of("d2", "a2", "b1", "b3", "c")
.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: d2
// filter: a2
// map: a2
// forEach: A2
// filter: b1
// filter: b3
// filter: c
现在,map
只会调用一次,所以操作流水线对于更多的输入元素会执行更快。在整合复杂的方法链时,要记住这一点。让我们通过添加额外的方法sorted
来扩展上面的例子:
Stream.of("d2", "a2", "b1", "b3", "c")
.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: a2; d2
sort: b1; a2
sort: b1; d2
sort: b1; a2
sort: b3; b1
sort: b3; d2
sort: c; b3
sort: c; d2
filter: a2
map: a2
forEach: A2
filter: b1
filter: b3
filter: c
filter: d2
首先,排序操作在整个输入集合上执行。也就是说,sorted
以水平方式执行。所以这里sorted
对输入集合中每个元素的多种组合调用了八次。我们同样可以通过重排调用链来优化性能:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
}).sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
}).map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
}).forEach(s -> System.out.println("forEach: " + s));
// filter: d2
// filter: a2
// filter: b1
// filter: b3
// filter: c
// map: a2
// forEach: A2
这个例子中sorted
永远不会调用,因为filter
把输入集合减少至只有一个元素。所以对于更大的输入集合会极大提升性能。