java Stream流操作

前言

    在Java8中另外一个比较大且非常重要的改动就是Stream。它规范及简化了程序的统计过程。举个例子:

//过程式的解决方案
int longest = 0;
for(String str : strings){
    if(str.startsWith("A")){// 1. filter(), 保留以A开头的字符串
        int len = str.length();// 2. mapToInt(), 转换成长度
        longest = Math.max(len, longest);// 3. max(), 保留最长的长度
    }
}

//stream流的解决方案
int longestStringLengthStartingWithA
        = strings.stream()
              .filter(s -> s.startsWith("A"))
              .mapToInt(String::length)
              .max();

stream流的解决方案解开了代码细节和业务逻辑的耦合,类似于sql语句,表达的是"要做什么"而不是"如何去做",使程序员可以更加专注于业务逻辑,写出易于理解和维护的代码。

Stream-操作分类

作为一计算框架,其基础是操作拆解,归类。常用操作分类:

在这里插入图片描述
Stream上的所有操作分为两类:中间操作和结束操作。

  • 中间操作只是一种标记(只有结束操作才会触发实际计算)。中间操作又可以分为无状态的(Stateless)和有状态的(Stateful)。
    • 无状态中间操作是指元素的处理不受前面元素的影响,
    • 有状态的中间操作必须等到所有元素处理之后才知道最终结果,
  • 结束操作,会触发实际计算(带动中间操作).其又可分为短路操作和非短路操作。
    • 短路操作,是指不用处理全部元素就可以返回结果.
    • 非短路操作,要全部执行完成.

有如此精细的划分,底层可以对每一种情况的处理方式不同。

Stream-执行计划的构建

当仅仅当“结束操作”的时,才会进行整个流程的计算。Stream是如果保存串联“中间操作”?
Stream完整的中间操作定义成<数据来源,操作,回调函数>的三元组,双用双向链表的方式构建。
在这里插入图片描述
图中通过Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的Stream。这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。这就是Stream记录操作的方式。

操作叠加

针对无状态和有状态及断路操作,JDK在回调函数sink上进行了优化。对Sink接口包含的方法如下表所示:

方法名作用
void begin(long size)开始遍历元素之前调用该方法,通知Sink做好准备
void end()所有元素遍历完成之后调用,通知Sink没有更多的元素了
boolean cancellationRequested()是否可以结束操作,可以让短路操作尽早结束
void accept(T t)遍历元素时调用,接受一个待处理元素,并对元素进行处理
  1. 对于filter,直接在accept中把符合条件的数据传给下一个sink
  2. 对于sorted,begin时创建数组,accept插入排序,end时触发下一个
  3. 对于findAny,直接让cancellationRequested为true,结束传递
操作执行

Sink完美封装了Stream每一步操作,并给出了[处理->转发]的模式来叠加操作。这一连串的齿轮已经咬合,就差最后一步拨动齿轮启动执行。是什么启动这一连串的操作呢?也许你已经想到了启动的原始动力就是结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。


//ReferencePipeline::collect  
//AbstractPipeline::evaluate  
//ReduceOp::evaluateSequential
//AbstractPipeline::wrapAndCopyInto
//AbstractPipeline::wrapSink

/**
 *   @param sink 表示Terminal Operation的sink
 *   return 表示第一个sink
**/
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
    for (AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
        sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
    }
    return (Sink<P_IN>) sink;
}

//ReferencePipeline::collect  
//AbstractPipeline::evaluate  
//ReduceOp::evaluateSequential
//AbstractPipeline::wrapAndCopyInto
//AbstractPipeline::copyInto

/**
 *   @param wrappedSink 表示第一个sink
 *   @param spliterator 数据源 
**/
 final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
        Objects.requireNonNull(wrappedSink);
        
        //如果没有短路操作
        if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
            wrappedSink.begin(spliterator.getExactSizeIfKnown());
            spliterator.forEachRemaining(wrappedSink);
            wrappedSink.end();
        }
        else {
            //有短路操作
            copyIntoWithCancel(wrappedSink, spliterator);
        }
    }

上述代码首先调用wrappedSink.begin()方法告诉Sink数据即将到来,然后调用spliterator.forEachRemaining()方法对数据进行迭代Spliterator,最后调用wrappedSink.end()方法通知Sink数据处理结束

Spliterator 是容器的一种迭代器

结果收集

根据需要的结果不同,通过构建不同Terminal Operation的sink来实现的

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

并行原理

以上讲的是都是基于串行的。Java Stream还提供了并行的支持。相比串行操作,并行执行中的难度为:有状态中间操作。串行操作可以很方面的利用sink的begin和end方法汇聚数据,执行。而并行呢?Java Stream则将整个链路以“有状态中间操作” 进行split.

//ReferencePipeline::collect  
//AbstractPipeline::evaluate  
//AbstractPipeline::sourceSpliterator
private Spliterator<?> sourceSpliterator(int terminalFlags) {
    //如果是并行流并且有stage包含stateful操作
    if (isParallel() && sourceStage.sourceAnyStateful) {
        int depth = 1;
        //那么就会依次遍历stage,直到遇到stateful stage时
        for (AbstractPipeline u = sourceStage, p = sourceStage.nextStage, e = this;
             u != e;
             u = p, p = p.nextStage) {
            //p是有状态操作
            if (p.opIsStateful()) {
                depth = 0;
                //尽量以惰性求值的方式进行操作,将前面一半执行得出结果
                spliterator = p.opEvaluateParallelLazy(u, spliterator);
            }
            p.depth = depth++;        
        }
    }
    return spliterator;
}

在这里插入图片描述

任务如何拆分和存储

借助于Fork/Join框架,把任务按以下规则进行拆分。
在这里插入图片描述
每个任务产生的数据都将成为(Node是一种多叉树结构sink)的一个child,最后通过合并child成最终的结果。

主要参考

深入理解Java Stream流水线
Java8中Spliterator详解
java8Stream原理深度解析

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值