《流》 —— Java Stream原理解析,它为什么这么简洁又这么好用?

3,583 阅读8分钟

大家好,我是方圆,这篇博客的写作思路来源于文末的深入理解Java函数式编程和Streams API ,看了几遍,真的是很经典的一篇,本篇博客是对其内容从我自己理解的角度进行扩展和细节上的解析,如果大家能把它通读下来并debug源码,应该能通透了!这篇博客也算得上是锦上添花,不过这都在有的基础上,读完本篇再读读文末的参考博客,相信会更好


1. 准备工作

  • 了解中间操作结束操作 Stream的所有操作分为以上两类,中间操作仅仅是一种标记,它的返回值仍然是Stream,这也是Stream能进行链式编程的原因,中间操作分为有状态无状态两种,无状态的操作不受前面元素的影响,而有状态的操作必须等所有元素处理完后才知道最终的结果,比如排序sorted()操作,它在没有处理完所有元素时,是不能知道最后的排序结果的,中间操作如下图所示

在这里插入图片描述

结束操作是触发Stream触发计算,进行执行的根本,它分为短路操作非短路操作,短路操作在满足短路条件时,不需要处理完所有元素就能返回结果;而非短路操作是要处理完所有元素才返回结果,结束操作如下图所示

在这里插入图片描述

  • 创建一个People的类,其中的属性为姓名年龄,在后文中使用
class People<String, Integer> {
    private String name;
    private Integer age;

    public People(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public java.lang.String toString() {
        return "stream.People{" +
                "name=" + name +
                ", age=" + age +
                '}';
    }
}

2. Stream是如何运行的

分析这个问题,我们将从以下四个角度

  1. 操作是如何记录的?
  2. 操作是如何执行的?
  3. 执行后的结果(如果有)存放在哪里?

2.1 操作是如何记录的?

我们看如下一条代码,注意它并没有执行结束操作

List<People<String, Integer>> list = new ArrayList<>();
list.add(new People<>("wyl", 18));
list.add(new People<>("wyl", 23));

Stream<Integer> integerStream = list.stream().map(People::getAge).filter(x -> x % 2 == 0);
  • 先看list.stream(),我们debug跟进去,发现会调用Head的构造方法

在这里插入图片描述

Head的最终构造方法调用如下,其中previousStage属性很重要,是构建双向链表的关键,也就是说我们的一系列操作(Operation)会最终组成一个双向链表,而且我们这里发现,Stream将每个执行阶段命名为Stage,所以我们将在后文中经常提起Stage,大家知道Stage是Stream的每个执行阶段就好

在这里插入图片描述

刚刚提到构建双向链表,那么在构建双向链表的时候,有previousStage,那么也得有nextStage呀,我们看看AbstractPipeline中有没有这个字段,如下图,具体解释如图所示

在这里插入图片描述

  • 跟完了list.stream(),我们跟一下map(People::getAge),我们可以发现,调用的是StateLessOp的构造方法,StateLessOp代表的就是无状态的中间操作,其构造函数有一个重要的参数,upstream它是我们刚刚创建的HeadStage对象,如下图,也就是说,这个无状态的操作的上游节点是头结点

在这里插入图片描述

我们看看它的最终构造方法调用,我们可以发现,现在HeadStage和MapStage已经构成了双向链表,而且depth这里也有了解释,每多一个执行阶段,depth + 1,depth记录的是我们执行中间操作的次数

在这里插入图片描述

用图来表现如下

在这里插入图片描述

  • 最后我们还剩下filter(x -> x % 2 == 0),其实它的逻辑和上方执行逻辑一致,不再赘述,追后的链表如下所示,其中depth为2

在这里插入图片描述

  • 我们总结一下, stream()方法得到的是HeadStage,之后每一个操作(Operation)都会创建一个新的Stage并以双向链表的形式结合在一起,每个Stage都记录了本身的操作,Stream就以此方式,实现了对操作的记录,注意结束操作不算depth的深度,它也不属于stage,但是我们的示例语句中没有写结束操作的代码,所以在这里提一下

  • 接口和类的继承关系如下,我们可以看到有HeadStatelessOp对应无状态操作的Stage对象,StatefulOp对应有状态操作的Stage对象,Head和中间操作都是类图中的ReferencePipeline类

在这里插入图片描述

  • Stream的Lazy机制 当该语句执行完的时候,我们在debug的过程中,并没有发现它进行执行任何map或者filter的逻辑,list也没有被改变,这就是Stream的Lazy机制。它的特点是:Stream直到调用终止操作时才会开始计算,没有终止操作的Stream将是一个静默的无操作指令

2.2 操作是如何执行的?

  • 我们将之前的语句加上结束操作,看它是如何执行的
List<Integer> ageList = list.stream()
			.map(People::getAge)
			.filter(x -> x % 2 == 0)
			.collect(Collectors.toList());
  • 直接进入collect()方法,发现需要先执行makeRef()方法

在这里插入图片描述

在这里插入图片描述

随后我们看看evaluate()方法,因为我们是串行执行,所以会执行图中框选的方法,参数是结束操作,在本例中也就是collect操作

在这里插入图片描述

  • 进入该方法后,我们可以看见一个makeSink()方法,好戏要开始了

在这里插入图片描述

在这里插入图片描述

这个方法它会返回一个Sink对象,而在这个Sink对象中,它会封装结束操作,随后进入下图中wrapAndCopyInto()方法,其中sink参数就是我们的结束操作Sink,下面儿我们重点看wrapSink方法

在这里插入图片描述

  • wrapSink()方法 在这里插入图片描述

这个方法中的for循环非常关键,p对应的是中间操作的节点,之前我们知道,我们创建了两个Stage,第一个是mapStage,它对应的depth为1,第二个是filterStage,它对应的depth为2,结束操作不会产生Stage,所以该循环会执行两次,它会先从最后一个Stage开始执行,根据其中的previousStage指针来找到前一个节点继续执行,而循环中的opWrapSink方法将会构造出一个Sink的套娃

在这里插入图片描述

它执行的是Sink中的ChainedReference中的构造方法,如下,当前Sink(封装有filter操作)会通过downstream字段保存结束操作(collect操作),也就是说Sink可以通过downstream字段找到下一个需要执行的操作

在这里插入图片描述

当循环执行完后,形成的Sink套娃如下图所示,最终for循环返回的Sink引用是最外层的Sink引用

在这里插入图片描述

  • 下面来看真正的执行阶段

在这里插入图片描述

map的下游是filter,filter继续调用begin方法,通知collect做准备

在这里插入图片描述

collect执行完begin会创建一个ArrayList做为数据准备,因为我们写的是collect(Collectors.toList())

在这里插入图片描述在这里插入图片描述

数据准备做完之后,开始真正的执行操作,spliterator是一个迭代器,来迭代其中的值

在这里插入图片描述

action.accept是关键

在这里插入图片描述

它会先执行map的操作,之后通过下游downstream调用filter的操作,再通过下游downstream调用结束操作的accept方法(也就是向list执行add方法进行添加符合要求的值)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

对其他元素也进行如上处理,处理完后执行end方法,结束操作

在这里插入图片描述

我们做一下总结

  1. 它会从最后一个中间操作开始进行Sink封装,每个Sink的downstream属性来保存下一个阶段需要执行的操作,第一个封装的Sink的downstream引用的是结束操作,直到所有中间操作封装完成后,返回一个Sink套娃
  2. 对Sink套娃从第一个开始,执行begin方法做好数据准备
  3. 之后通过accept方法对元素进行处理,中间操作会不断调用该方法
  4. 最后end方法告知Sink所有操作执行完毕

  • 为什么要使用Sink再进行封装,使用之前的Stage构成的双向链表执行不行吗?

Sink中的方法如下图所示 在这里插入图片描述

每个Stage保存的只是当前节点的操作,不能够在节点间进行协调,而有了Sink里的四种通讯方法,方便了相邻的Stage的调用,实际上Stream API内部实现的的本质,就是如何重写Sink的这四个接口方法

2.3 执行后的结果(如果有)存放在哪里?

在这里插入图片描述

  1. 对于表中返回boolean或者Optional的操作(Optional是存放 一个 值的容器)的操作,由于值返回一个值,只需要在对应的Sink中记录这个值,等到执行结束时返回就可以了。
  2. 对于归约操作,最终结果放在用户调用时指定的容器中(容器类型通过收集器指定)。collect(), reduce(), max(), min()都是归约操作,虽然max()和min()也是返回一个Optional,但事实上底层是通过调用reduce()方法实现的。
  3. 对于返回是数组的情况,毫无疑问的结果会放在数组当中。这么说当然是对的,但在最终返回数组之前,结果其实是存储在一种叫做Node的数据结构中的。Node是一种多叉树结构,元素存储在树的叶子当中,并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便。

巨人的肩膀