什么是延迟操作?来仿写一个Stream API就知道啦

220 阅读7分钟

之前写的 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

gitee.com/bravo1988/d…