探究一下Java Stream的架构设计与源码实现。
场景介绍
首先来看一段典型代码:
Arrays.asList("a", "b", "c").stream()
.map(x -> x.toUpperCase())
.filter(x -> !x.equals("b"))
.forEach(x -> System.out.println(x));
从代码中看到,List#stream()创建对应的Stream实例,然后进行链式调用,依次为map()、filter()、forEach(),其中我们要执行的逻辑以参数的形式传递到对应操作中这样就能得到最终的结果。
实际上,map()、filter()并不会触发逻辑的执行,这类操作被称为中间操作,又分为两类:StatefulOp和StatelessOp;真正触发操作执行是forEach(),被称为终止操作TerminalOp。
每一个中间操作都会生成一个新的Steam节点,而pipline正是这一个个节点连接而成的双向链表结构。
Stream节点继承体系
上文提到的Stream节点,在代码中继承结构如下:
分别介绍一下:
- BaseStream定义流操作基本操作
- AbstractPipline是BaseStream的抽象实现类
- Stream定义流所有的中间操作和终止操作
- ReferencePipline是Stream的抽象实现类,并继承了AbstractPipline
- Head是具体的类,定义流的起点
- StateFulOp是抽象类,定义了有状态的中间操作
- StateLesOp是抽象类,定义了无状态的中间操作
其中,AbstractPipline的一个重要的作用就是串联不同的Stream节点(也叫Stage),形成一个双向链表结构。
代码如下:
AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
if (previousStage.linkedOrConsumed)
throw new IllegalStateException(MSG_STREAM_LINKED);
previousStage.linkedOrConsumed = true;
previousStage.nextStage = this;
this.previousStage = previousStage;
this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK;
this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags);
this.sourceStage = previousStage.sourceStage;
if (opIsStateful())
sourceStage.sourceAnyStateful = true;
this.depth = previousStage.depth + 1;
}
逻辑图如下:
Sink链的构建
上图不仅展示map()、filter()等转换操作所形成的双向链表结构,同时也展示了终止操作forEach()所形成的Sink调用链。
终止操作,在抽象类ReferencePipline中实现,具体实现逻辑委托给对应的TerminalOp,TerminalOp依赖对应的操作类来产生实例。如:foreach()操作,委托给ForEachOpsmakeRef产生的TerminalOp。
在TerminalOp的执行过程中会产生Sink调用链,核心代码如下: AbstractPipeline#wrapSink()
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
Objects.requireNonNull(sink);
for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
}
return (Sink<P_IN>) sink;
}
从代码中可以看到,从尾部开始依次调用每个Stream节点的opWrapSink(),在这个过程中实际是使用头插法形成了一个单向链表结构。
这里不得不提Head节点的创建,实际依靠两个辅助类:
- StreamSupport工具类,主要用于创建流中的头节点Head。
- Spliterators工具类,定义了一系列重载的spliterator()方法,返回具体的Spliterator,Spliterator执行数据源的拆分和遍历。
Spliterator实际是保存了集合数据,在Sink链创建完成后就会执行Spliterator#forEachRemaining(),并且把Sink链的头节点作为参数传递进去:
public void forEachRemaining(Consumer<? super E> action) {
int i, hi, mc; // hoist accesses and checks from loop
ArrayList<E> lst; Object[] a;
if (action == null)
throw new NullPointerException();
if ((lst = list) != null && (a = lst.elementData) != null) {
if ((hi = fence) < 0) {
mc = lst.modCount;
hi = lst.size;
}
else
mc = expectedModCount;
if ((i = index) >= 0 && (index = hi) <= a.length) {
for (; i < hi; ++i) {
@SuppressWarnings("unchecked") E e = (E) a[i];
action.accept(e);
}
if (lst.modCount == mc)
return;
}
}
throw new ConcurrentModificationException();
}
核心逻辑是每取到一个元素,就调用action#accept()方法,实际上,就会依次执行sink链上的每一个节点的逻辑,即我们之前通过map()、filter()、forEach()等操作传递进去的逻辑,最终实现了集合元素的转换、过滤、打印。
代码及设计模式
代码大量使用静态内部类、匿名内部类,并且大量使用了泛型、函数式编程使代码精简;Steam链、Sink链的设计很巧妙;延迟执行机制及迭代调用提高性能。总体来说,代码量不大但设计很巧妙以及各种技术的灵活应用,有一定阅读难度。
设计模式:
- 模板方法模式
- 简单工厂模式:获取TeminalOp的过程
思考一个问题:提前结束数据源循环过程?
答案是否定的。 变通的方式是:在尽可能早的位置使用filter()操作,过滤掉不必要的数据,后续的操作就不会执行了,从而节省计算资源。