Java Stream原理解析(二)

1、Stream Pipelines

上一篇文章对Stream流做了简单介绍,并遗留下几个小问题,其中一个是Stream流在结束操作会触发实际计算,在计算发生的时候会把所有的中间操作积攒操作pipeline的方式进行,那此处pipeline到底是怎么样的方式那,下面我们用一个简单的例子来一步一步的理解。

栗子:从一个字符串列表中找到以‘a'开头的,最长的字符串长度。一种简单的方式是每一次函数调用的时候都执行一次迭代,这样并将处理中间结果放到某种数据结构中(比如数组、容器等)。

for循环的处理形式:

List<String> strings = Arrays.asList("a", "bb", "ccc", "abcd");

List<String> startWithAList = new ArrayList<>();
for (String string: strings) {
    if(string.startsWith("a")){
        startWithAList.add(string); // 1. filter(), 保留以A开头的字符串
    }
}

List<Integer> lengths = new ArrayList<>();
for (String string : startWithAList) {
    lengths.add(string.length());   // 2. mapToInt(), 转换成长度
}
        
int maxLength = 0;
for(Integer length : lengths){
    maxLength = Math.max(length, maxLength);   // 3. max(), 保留最长的长度
}
assertEquals(4, maxLength);

我们可以根据for循环的处理形式,可以总结出Stream的中间操作是:调用filter()方法后立即执行,选出所有以A开头的字符串,之后传给mapToInt()方法并立即执行,之后遍历找出最大的数字作为最终的结果。

Stream流处理方式:

int maxLength = Stream.of("a", "bb", "ccc", "abcd")
                .filter(value -> value.startsWith("a"))
                .mapToInt(String::length)
                .max()
                .orElse(0);
assertEquals(4, maxLength);

我们结合上一篇的Stream流的特性可以看出,Stream流有效的避免存储中间结果。

从上面的例子我们可以看出,只要事先知道用户的意图,总是能够采用上述的Stream方式实现等价的for循环功能,但问题是Stream流的设计者并不知道用户的意图是什么,那么如何在无法假设用户行为的前提下实现流水线,是设计者所必须要考虑的问题。

2、Stream流水线的解决方案

通过上面的栗子,我们大致看到,Stream流应该采用某种方式记录用户每一步的操作,当用户调用结束操作时,之前记录的操作叠加在一起在一次迭代中全部执行完毕,沿着这个思路我们不难想到这中间需要解决如下问题:

用户的操作如何记录?

操作如何叠加?

叠加之后如的操作如何执行?

执行的结果在哪里?

在讨论这几个问题过程汇总,我们以下面例子为主:

Stream.of("onE", "twO", "threE", "fouR")
//    .parallel()
        .filter(e -> e.length() > 3)
        .map(String::toLowerCase)
        .sorted()
        .map(String::toUpperCase)
        .forEach(System.out::println);
  • 操作如何记录?

注意这里使用的“操作”,指的是“Stream中间操作”的操作。很多Stream操作会需要一个回调函数(Lambda表达式),因此一个完整的操作是<数据来源、操作、回调函数>构成的三元组。Stream中使用Stage的概念来描述一个完整的操作,并用某种实例化的后的PipelineHelper来代表Stage,将具有先后顺序的各个Stage连接起来,就构成了整条流水线。下图为Stage相关类和接口的继承关系图: 

图中的Head用于表示第一个Stage,即调用诸如Collection.Stream()方法产生的Stage,很显然这个Stage不包含任何操作,StatelessOp和StatefulOp分别表示无状态和有状态的Stage,对应于无状态和有状态的中间操作。

Stream流水线组织结构示意图如下:

通过Stream.of()方法得到Head Stage,紧接着调用一系列中间操作,不断生成新的Stream。这些Stream对象以双向链表结构组织在一起,构成整个流水线,由于每个Stage都记录前一个Stage和本次的操作以及回调函数,依靠三元结构就能够建立器对数据源的所有操作。这就是Stream记录操作的方式,

  • 操作如何叠加?

我们知道Stream是如何记录操作之后,要想让流水线起到应有的作用我们需要一种将所有操作叠加一起的方案。只需要从流水线的head开始依次执行每一步的操作(包括回调函数)就行了。这听起来似乎可行,但是你忽略了前面的Stage并不知道后面Stage到底执行哪种操作,以及回调函数是哪种形式。换句话说,只有当前Stage本身才知道该如何执行自己包含的动作。这就需要有某种协议调用相邻Stage之间的调用关系。

这种协议有Sink接口完成,Sink接口包含的方法如下表所示:

方法名作用
void begin(long size)开始遍历元素之前调用该方法,通知Sink做好准备。
void end()所有元素遍历完成之后调用,通知Sink没有更多的元素了。
boolean cancellationRequested()是否可以结束操作,可以让短路操作尽早结束。
void accept(T t)遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage只需要调用当前Stage.accept(T t)方法就行了。

通过上面的Sink协议,相邻的Stage之间调用很方便,每个Stage都会将自己操作封装到一个Sink里,前一个Stage只需要调用后一个Stage的accept()方法即可,并不需要知道内部是如何处理的。对于有状态的操作,Sink的begin()和end()方法也是必须实现的 。比如Stream.sorted()是一个有状态的中间操作,其对应的Sink.begin()方法可能创建一个容器,而accept()方法负责将元素添加到该容器,最后的end()负责对容器排序。对于短路操作,Sink.cancellationRequested()也是必须实现的,比如Stream.findFirst()的短路操作,只需要找到一个元素,cancellationRequested()就应该返回true。Sink的四个接口常常互相协作,共同完成计算任务。实际上Stream API内部的实现就是如何重载Sink的这四个接口方法。

有了Sink对操作的包装,Stage之间的调用问题就解决了,执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(),accept(),cancellationRequested(),end()}方法就可以了,以Sink.accept()方法举例:

void accept(U u){
    1. 使用当前Sink包装的回调函数处理u
    2. 将处理结果传递给流水线下游的Sink
}

Sink接口的其他几个方法也是按照【处理->转发】的模型实现。下面我们通过例子来看一下Stream中间操作是如何将自身的操作包装成Sink以及Sink是如何将处理结果转发给下一个Sink的。先看一下Stream.map()方法源码:

// java.util.stream.ReferencePipeline#map// 调用该方法将产生一个新的Stream
public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
   Objects.requireNonNull(mapper);
   return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
        /*opWrapSink()方法返回由回调函数包装而成Sink,由wrapSink()触发*/
        @Override
        Sink<P_OUT> opWrapSink(int flags, Sink<R> downstream) {
           return new Sink.ChainedReference<P_OUT, R>(downstream) {
               @Overridepublic void accept(P_OUT u) {
                   R r = mapper.apply(u);    // 1. 使用当前Sink包装的回调函数mapper处理u
                   downstream.accept(r);    // 2. 将处理结果传递给流水线下游的Sink
               }
           };
       }
   };
}

我们从源码不难看出回调函数mapper包装成一个Sink中,由于Stream.map()是一个无状态的中间操作,所以map()方法返回了一个StatelessOp的内部类对象,通过调用新的opWrapSink()方法将得到一个包装了当前操作回调函数的Sink。下面是关于Sink.ChainedReference的源码:

static abstract class ChainedReference<T, E_OUT> implements Sink<T> {
    protectedfinal Sink<? super E_OUT> downstream;

    public ChainedReference(Sink<? super E_OUT> downstream) {
        this.downstream = Objects.requireNonNull(downstream);
    }

    @Overridepublic void begin(long size) {
        downstream.begin(size);
    }

    @Overridepublic void end() {
        downstream.end();
    }

    @Overridepublic boolean cancellationRequested() {
        return downstream.cancellationRequested();
    }
}

我们在来看一个例子,Stream.sorted()方法将对Stream中的元素进行排序,显然这是一种有状态的中间操作,因为读取所有元素之前是没有办法得到最终顺序的。排开模板直接进入问题本质,sorted()方法是如何将操作封装成Sink?sorted一种可能封装的Sink代码如下:

// java.util.stream.SortedOps.RefSortingSink:371行
// Stream.sorted()方法用到的Sink实现,由java.util.stream.SortedOps.OfRef#opWrapSink:133行触发
class RefSortingSink<T> extends AbstractRefSortingSink<T> {
    private ArrayList<T> list;// 存放用于排序的元素
    RefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
        super(downstream, comparator);
    }
    @Override
    public void begin(long size) {
        ...
        // 1. 创建一个存放排序元素的列表
        list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
    }
    @Override
    public void end() {
        list.sort(comparator);// 3. 只有元素全部接收之后才能开始排序
        downstream.begin(list.size());
        if (!cancellationWasRequested) {// 下游Sink不包含短路操作
            list.forEach(downstream::accept);// 将处理结果传递给流水线下游的Sink
        }
        else {// 4. 下游Sink包含短路操作
            for (T t : list) {
                // 每次都调用cancellationRequested()询问是否可以结束处理。
                if (downstream.cancellationRequested()) break;
                downstream.accept(t);// 将处理结果传递给流水线下游的Sink
            }
        }
        downstream.end();
        list = null;
    }
    @Override
    public void accept(T t) {
        list.add(t);// 2. 使用当前Sink包装动作处理t,只是简单的将元素添加到中间列表当中
    }
}

从代码种我们不难看出Sink的四个接口方法的协同:首相begin()方法告诉Sink参与排序的元素个数,便于确定中间结果容器的大小,之后通过accept()方法将元素添加到中间结果,最终执行时调用这会不断的调用该方法,知道遍历所有元素,最后end()方法告诉Sink所有元素遍历完毕,启动排序步骤,排序OK之后,将结果转递给下游的Sink,转递给下游的时候,根据下游Sink是否需要短路操作进行不同的操作。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值