写在前面
众所周知啊,java的Stream非常好用!其链式的代码结构,让一个变量起名困难症的资深患者(我)十分沉迷,就此抛弃了for。
Ps. 就某些操作(异常处理,操作stream外变量等)来讲,还是写for更方便。
之前一直把Stream当做一个链式的迭代器来用,但Stream就仅仅是一个迭代器吗?
奥夫靠斯闹特! 记录一下自己走出盲目自信(不好好学)的误区。
起因
在一个略(lei)感(de)繁(yao)忙(si)的工作日,正在写一个非常简单的需求,找到List中第一个符合条件的元素,简单写一下for的实现
public static void main(final String[] args) {
final List<Integer> numbers = List.of(1, 2, 3);
System.out.println(findGreaterThanOne(numbers));
}
private static Integer findGreaterThanOne(final List<Integer> numbers) {
for (final Integer number : numbers) {
if (number > 1) {
return number;
}
}
return null;
}
非常的简单是不是,但是呢,作为一个Stream爱好者,非常的抗拒使用for,于是写了一个Stream版,写的时候发现了一些问题
private static Integer findGreaterThanOne(final List<Integer> numbers) {
return numbers.stream() // 在我的认知里,这里返回[1,2,3]
.filter(number -> number > 1) // 这里过滤后,剩下[2,3]
.findFirst() // 找到第一个,返回2
.orElse(null);
}
写完之后发现,相较于for,元素3多执行了一次判断。基于对Stream的执着,没有直接改成for,咨询了一下组里的大哥,大哥说加个peek(注①)看看执行过程。
结果不试不知道,一试吓一跳,直接颠覆了我对Stream的认知(学艺不精)。修改一下代码
private static Integer findGreaterThanOne(final List<Integer> numbers) {
return numbers.stream()
.filter(number -> number > 1)
.peek(number -> System.out.println(number))
.findFirst()
.orElse(null);
}
执行前,想定的输出结果是
2
3
实际的输出结果却是
2
元素3并没有多执行了一次判断,这让我反思起来我对Stream的认知。
分析
先梳理一下,方便大家理解。
元素扩展为5个,filter增加到4个,看上去明显一些
public static void main(final String[] args) {
final List<Integer> numbers = List.of(1, 2, 3, 4, 5);
System.out.println(findGreaterThanOne(numbers));
}
private static Integer findGreaterThanOne(final List<Integer> numbers) {
return numbers.stream()
.filter(number -> number > 1)
.filter(number -> number > 2)
.filter(number -> number > 3)
.filter(number -> number > 4)
.findFirst()
.orElse(null);
}
代码执行顺序如下(图片中处理步骤有一些错误,只看处理顺序即可)
看图的话可以清晰的分辨两者的差别,是我把Stream想简单了。
整理一下我认知偏差的原因,按照常规的代码逻辑各个.filter()之间没有直接联系,会在执行完前一步后执行下一步操作。
举个例子,用js的数组写一下上述逻辑
可见常规的遍历符合我之前的认知。js的数组会在执行完上一句函数后,才能继续执行。
而Stream却是收集所有处理后,按照元素遍历,依次执行。这和传统的遍历大不相同,有必要研究一下Stream的运行机制了。
思考
在调查Stream实现原理前,先将自己代入一下Stream开发者的视角,如果是要我完成这个集中处理需求,该怎么实现呢。不感兴趣的小伙伴们可以略过。
首先,将Stream的方法分为两种。一种返回Stream对象(map、filter、peek等),简称为中间操作。一种不返回Stream对象(foreach、findFirst、colloct等),简称为终止操作。
这和Builder构造器的设计思想类似,Builder只有执行build方法时才实际执行。
Builder.setA()
.setB()
.setC()
.build();
调用非截流方法时,将其保存在Stream内。调用截流方法时,执行相应处理。简单写一下伪代码(仅思路)
class Stream<T> {
private List<T> items;
private List<Predicate | Function ...> handlers;
public map(Function function) {
handlers.add(function);
}
public filter(Predicate predicate) {
handlers.add(predicate);
}
public foreach(Consumer consumer) {
for (item : items) {
for (handler : handlers) {
if (handler instanceof Function) {
item = handler.apply(item);
}
if (handler instanceof Predicate) {
if (handler.apply(item)) {
break;
}
}
consumer.apply(item);
}
}
}
}
调查
其实这一部分写了一些之后,在翻看站内其他文章时,感觉自己写的简陋且生硬,还是推荐大家去搜索一下站内的优秀文章(详见注②),这里我简单说明一下,方便懒得看文章的小伙伴们理解。
核心功能:
1,Spliterator:分离器,负责遍历元素,还有一些并行处理的功能,在终止操作时调用
2,Pipeline+Sink:保存中间操作的链表,在执行终止操作时构建流水线
3,evaluate方法:终止操作,在不同的终止操作(foreach、findFirst等)实现各自的逻辑
到这里,相信大家已经明白了Stream的运行机制,有兴趣研究的话,可以进一步深入。
总结
总得来讲,Stream不仅仅只是链式操作的迭代器,其中包含的设计思路值得我们深入思考,以拓宽我们在其他方面的设计思路。
注
①,peek是一种不操作数据的流操作,多用于调试、log记录等
②,Java8 Stream源码精讲