之前写的 FluentStream 有一些缺点:
- 不够简单明了
- 不够贴近原版
我相信部分读者应该都没读完那篇文章,设计思路确实有点绕,不仅设计思路和 Java 8 Stream API 不同,执行的现象也有所不同。
比如下面这个测试案例:
FluentStream 得到的结果是:
而 Stream API 是这样的:
发现问题了吗,FluentStream 是所有元素同时从起跑线出发逐个淘汰,而 Stream API 则是元素逐个跑完全程。
现在让我们新写一个版本,设计思路稍微贴近原版一些。
手写Stream版本1
很简单,应该都能看懂。然后给上面的Stream写一个测试(故意把filter和forEach拆成两段代码):
在终端操作forEach 执行前,filter已经开始处理元素了,不符合 Stream API 的定义:
只有当一个终端操作被调用时,例如forEach()
、collect()
、reduce()
等,Stream管道(pipeline)才会被启动。
手写Stream版本2
版本2比版本1更好理解,而且效果也更好。同样的测试案例,得到的结果是:
手写Stream版本3
如果再来一个map(),难道要这样?
再来一个peek()呢?再定一个变量保存peek操作?很显然,为每一种操作定义一个变量来存储的做法是不可行的。
代码迭代到这,从面向对象的角度,一般会倾向于抽象。同为中间操作,filter看起来和map有很大的不同,如果把peek也算进来,可真的算是一锅乱炖:
- filter:接受一个item,返回boolean
- map:接受一个item,返回另一个类型的item
- peek:接受一个item,做一些操作,不返回
返回值完全不同,甚至有些操作没有返回值,似乎不好做函数抽象。但是换个角度,这些中间操作就像一节节下水管道,水至上而下流过,只有两种可能:
- 流不下来(蒸发了)
- 流下来了(不管发生了什么化学反应,变成了什么)
所以,这里打算抽象出一个Stage。
private interface Stage {
Object accept(Object item);
}
为了简单起见,就不整泛型了,看着烦。
FilterStage和MapStage长啥样呢?
很简单对吧,Stage也没什么大不了的,就是把predicate和mapper包一下,不管内地的、香港的、台湾的,纵然有这样或那样的不同,都是中国人对吧。用一个更大的抽象概念,去求同存异即可。
既然都是Stage,自然就可以用List去统一存储所有中间操作。
重点看forEach:遍历每一个元素,为每个元素先执行中间操作、再执行终端操作,如果没有终端操作,中间操作不会启动(只是被存起来而已)。
再来测试一把:
李健被filter了,所以没有后续操作。元素也确实是逐个执行,并且仅遍历一次。
手写Stream版本4
为了稍微接近Java 8的 Stream API,这里再做一次迭代:优化forEach中的process。
process的逻辑是,对当前遍历到的元素,逐个执行所有中间操作,内部用的是for循环。现在我要把它改成一种类似链式结构, 你也可以叫它责任链,anyway。
怎么引入链式结构呢?这取决于你期望借助链式结构达到什么效果。
比如,我期望最终的写法是这样的:
先别管Sink是什么,你可以把它看作另一种Stage,也是对中间操作的封装。总之,通过wrapStages(),我们得到了一种链式结构,这条链上有我们通过filter()、map()等方法压入的中间操作,只要调用 sink.accept(element) ,element就会沿着链条执行下去,就像往下水道倒了一碗水。
开始实施。
目前:stage负责封装中间操作
目标
- stage:组装链式结构
- sink:封装中间操作、当前操作执行结束后,将流程推向下一个
发现了吗,原本stage的职责转移给了sink,它有了新的职责:组装链式结构。
链式结构怎么来的
filter()、map()等操作只做存储,不做组装,等到 forEach 触发后,在遍历元素之前调用 wrapStages() 组装链式结构。
以 filterStage 为例:
也就是说,wrap干了两件事:
- 创建并返回新节点
- 通过闭包,在新节点内部维持对旧节点的引用
-
- forEach
- filter -> forEach
- map -> filter -> forEach
链式操作如何执行
调用 forEach 开启管道操作:
public void forEach(Consumer<? super T> action) {
Sink sink = wrapStages(new ForEachSink<>(action));
for (T element : source) {
// 上面组装成功后,开始遍历元素:把元素倒入下水道
sink.accept(element);
}
}
accept 是管道操作的入口,从 filter 开始:
代码已上传到 git 仓库:com/bravo/advanced/fluent/stream2