• Java8 Stream源码精讲(二):Stream创建原理深度解析


    简介

    Java8 Stream源码精讲(一):从一个简单的例子入手

    上一篇文章,通过分析一个使用Stream操作数据的例子,讲解了构建Stream,经过中间操作map()和filter()方法调用返回一个ReferencePipeline链表,调用终止操作forEach()将声明的函数构造成为一个sink链表,最终每一个元素都会被传入Sink#accept()方法处理。本章将通过重点分析创建Stream的源码,了解Stream的构建过程。

    Spliterator是什么

    在分析Stream构建之前,需要填一下上一章的坑,还记得吗,在上一章分析Stream流程的时候,构建Stream传入了一个Spliterator对象,当时只是说它是一个类似迭代器一样的东西。

    public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive) { 
        return StreamSupport.stream(spliterator(array, startInclusive, endExclusive), false); 
    }
    复制代码

    现在我们来仔细看看这个接口是做什么的,先来看接口定义,省略了接口中的常量和内部接口,只留下了方法定义:

    public interface Spliterator<T> {
    
    //用于遍历单个元素,action是Stream调用终止操作之后包装的sink链
    boolean tryAdvance(Consumer&lt;? super T&gt; action);
    
    //用于批量遍历元素,action是Stream调用终止操作之后包装的sink链
    default void forEachRemaining(Consumer&lt;? super T&gt; action) {
        do { } while (tryAdvance(action));
    }
    
    //并行计算的时候拆分Spliterator
    Spliterator&lt;T&gt; trySplit();
    
    //预估元素的大小
    long estimateSize();
    
    //精确获取元素的大小
    default long getExactSizeIfKnown() {
        return (characteristics() &amp; SIZED) == 0 ? -1L : estimateSize();
    }
    
    int characteristics();
    
    default boolean hasCharacteristics(int characteristics) {
        return (characteristics() &amp; characteristics) == characteristics;
    }
    
    //获取元素比较器
    default Comparator&lt;? super T&gt; getComparator() {
        throw new IllegalStateException();
    }
    

    }
    复制代码

    Spliterator翻译成中文是分离器或者拆分器,接口注释大概的意思是:Spliterator是一个用于遍历和划分源元素的对象,源元素可以来自一个数组、Collection集合、IO Channel或者一个生成器函数。可以通过tryAdvance()方法来遍历源中的单个元素,也可以通过forEachRemaining()方法批量遍历源中的元素。在并行计算中,可以通过trySplit()方法将源中的一些元素拆分为另外的Spliterator。

    从注释我们知道Spliterator的主要作用:

    • 封装源元素
    • 遍历源元素
    • 拆分源元素(并行计算,不用关心)

    现在我们主要来看看方法:

    • tryAdvance():用于遍历源中的单个元素,如果源中还有元素存在,就调用参数action的accept()方法消费元素,并且返回true,否则返回false。
    • forEachRemaining():批量遍历源中的元素,调用参数action的accept()方法消费剩下的所有元素。
    • trySplit():用于拆分元素的,并行计算时会使用到,我们只关注串行流,所以不用关心。
    • estimateSize():预估元素的大小,如果元素大小是已知的,返回元素大小,如果流是无界的、大小未知的或者计算太耗时,则返回Long.MAX_VALUE。
    • getExactSizeIfKnown():如果元素大小是已知的,则返回精确大小,否则返回-1。
    • getComparator():如果源中的元素是被比较器排序的,则返回这个比较器;如果是按照自然顺序排序的,则返回null;否则抛出IllegalStateException异常。

    如何实现一个Spliterator

    对Spliterator有一个详细的认识之后,我们来看一看如何实现一个Spliterator。

    T8M{NF(4Z@3R0@`MB%C395F.png

    哇,好多实现类,第一时间是不是比较懵逼,这么多子类不知道从何下手?不过从图中可以看出,Spliterator的子类基本上都是某一个集合的内部类,所以我打算选择两个常用子类详细讲解,大家也可以根据这种思路分析其它的。

    ArraySpliterator

    ArraySpliterator是Spliterators的一个内部类,每次通过一个数组构建Stream时,都会创建相应的ArraySpliterator对象。Arrays#stream()方法的调用流程:

    public static <T> Stream<T> stream(T[] array) {
        return stream(array, 0, array.length);
    }
    

    public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive) {
    return StreamSupport.stream(spliterator(array, startInclusive, endExclusive), false);
    }

    public static <T> Spliterator<T> spliterator(T[] array, int startInclusive, int endExclusive) {
    return Spliterators.spliterator(array, startInclusive, endExclusive,
    Spliterator.ORDERED | Spliterator.IMMUTABLE);
    }
    复制代码

    Spliterators#spliterator()工厂方法用于创建ArraySpliterator对象:

    public static <T> Spliterator<T> spliterator(Object[] array, int fromIndex, int toIndex,
                                                 int additionalCharacteristics) {
        checkFromToBounds(Objects.requireNonNull(array).length, fromIndex, toIndex);
        //返回ArraySpliterator对象
        return new ArraySpliterator<>(array, fromIndex, toIndex, additionalCharacteristics);
    }
    复制代码

    ArraySpliterator字段分析:

    //源数组
    private final Object[] array;
    //当前索引
    private int index;        // current index, modified on advance/split
    //终止索引
    private final int fence;  // one past last index
    private final int characteristics;
    复制代码
    • array:源对象数组,在介绍Spliterator接口时说过,源元素可以来自一个数组。
    • index:当前数组的下标,当调用tryAdvance()、forEachRemaining()或者trySplit()方法时,会更改下标。
    • fence:数组的截止下标,一般也就是数组长度。

    ArraySpliterator构造函数

    public ArraySpliterator(Object[] array, int additionalCharacteristics) {
        this(array, 0, array.length, additionalCharacteristics);
    }
    

    public ArraySpliterator(Object[] array, int origin, int fence, int additionalCharacteristics) {
    this.array = array;
    this.index = origin;
    this.fence = fence;
    //这里要重点关注一下ArraySpliterator源元素大小是确定的
    this.characteristics = additionalCharacteristics | Spliterator.SIZED | Spliterator.SUBSIZED;
    }
    复制代码

    通过构造函数可以看出,默认情况下没有指明origin和fence时,就是从0开始,到数组尾部结束,数组中的元素都算作流元素。

    ArraySpliterator方法分析

    源码比较简单,直接在代码上用注释说明了,不再单独讲解。

    • forEachRemaining()
    public void forEachRemaining(Consumer<? super T> action) {
        Object[] a; int i, hi; // hoist accesses and checks from loop
        //判空
        if (action == null)
            throw new NullPointerException();
        //数组长度大于等于fence变量
        //index变量大于等于0
        //修改index变量且当前下标小于hi
        if ((a = array).length >= (hi = fence) &&
            (i = index) >= 0 && i < (index = hi)) {
            //循环消费数组元素
            do { action.accept((T)a[i]); } while (++i < hi);
        }
    }
    复制代码
    • tryAdvance()
    public boolean tryAdvance(Consumer<? super T> action) {
        //判空
        if (action == null)
            throw new NullPointerException();
        //当前下标大于等于0且小于fence
        //数组中才有剩余可访问的元素
        if (index >= 0 && index < fence) {
            //取元素,index自增
            @SuppressWarnings("unchecked") T e = (T) array[index++];
            //消费元素
            action.accept(e);
            return true;
        }
        //返回false代表没有源元素了
        return false;
    }
    复制代码
    • estimateSize()
    //就是fence减去index,代表数组中剩余的元素大小
    public long estimateSize() { return (long)(fence - index); }
    复制代码

    ArrayListSpliterator

    ArrayListSpliterator是ArrayList的内部类,调用ArrayList#stream()方法时会创建这样一个对象。

    ArrayList继承自Collection的stream()方法:

    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
    复制代码

    重写了spliterator()方法,这个方法会返回一个Spliterator对象,可以看到创建的就是ArrayListSpliterator,同时传入了ArrayList自身:

    public Spliterator<E> spliterator() {
        return new ArrayListSpliterator<>(this, 0, -1, 0);
    }
    复制代码

    那我们来着重研究下ArrayListSpliterator的源码。

    ArrayListSpliterator字段分析

    //源元素集合
    private final ArrayList<E> list;
    //当前下标
    private int index; // current index, modified on advance/split
    //结束下标
    private int fence; // -1 until used; then one past last index
    private int expectedModCount; // initialized when fence set
    复制代码
    • list:源元素集合,也就是创建ArrayListSpliterator时传入的ArrayList。
    • index:当前下标,当调用tryAdvance()、forEachRemaining()或者trySplit()方法时,会更改下标。
    • fence:结束下标,初始值为-1,当getFence()被调用时,会将ArrayList的size赋值给它,后面会讲到。
    • expectedModCount:看到这个变量是不是很眼熟,表示ArrayList在流处理中同样不能被并发修改。

    ArrayListSpliterator方法分析

    • forEachRemaining()
    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();
        //ArrayList不为空且其中存放元素的数组不能为空,很容易理解
        if ((lst = list) != null && (a = lst.elementData) != null) {
            //一般来说进入这个分支,因为创建ArrayListSpliterator时,fence是-1
            if ((hi = fence) < 0) {
                mc = lst.modCount;
                //hi就是ArrayList的元素大小
                hi = lst.size;
            }
            else
                mc = expectedModCount;
            //修改index变量
            if ((i = index) >= 0 && (index = hi) <= a.length) {
                for (; i < hi; ++i) {
                    //遍历list中的元素数组,取依次取上面的元素,调用action消费
                    @SuppressWarnings("unchecked") E e = (E) a[i];
                    action.accept(e);
                }
                //校验modCount,不允许方法执行时内部结构发生改变
                if (lst.modCount == mc)
                    return;
            }
        }
        throw new ConcurrentModificationException();
    }
    复制代码
    • tryAdvance()
    public boolean tryAdvance(Consumer<? super E> action) {
        //判空
        if (action == null)
            throw new NullPointerException();
        //这里hi其实就是ArrayList元素大小
        int hi = getFence(), i = index;
        if (i < hi) {
            index = i + 1;
            //取数组中index下标的元素
            @SuppressWarnings("unchecked") E e = (E)list.elementData[i];
            //调用action消费元素
            action.accept(e);
            //并发修改校验
            if (list.modCount != expectedModCount)
                throw new ConcurrentModificationException();
            return true;
        }
        //没有剩余的元素,返回false
        return false;
    }
    复制代码

    tryAdvance()中是通过将getFence()的返回值赋值给hi的,进入这个方法看下:

    private int getFence() { // initialize fence to size on first use
        int hi; // (a specialized variant appears in method forEach)
        ArrayList<E> lst;
        //首次被调用时会进入,将ArrayList.size赋值给fence,ArrayList.modCount赋值给expectedModCount,
        //其它时候直接返回fence的值
        if ((hi = fence) < 0) {
            if ((lst = list) == null)
                hi = fence = 0;
            else {
                expectedModCount = lst.modCount;
                hi = fence = lst.size;
            }
        }
        return hi;
    }
    复制代码
    • estimateSize()
    //估计值大小,ArrayList也是元素大小确定的,计算逻辑是ArrayList.size减去当前下标,表示还有多少源元素
    public long estimateSize() {
        return (long) (getFence() - index);
    }
    复制代码

    根据Spliterator创建Stream

    通过前面分析我们知道Stream都是通过StreamSupport这个工具类创建的,传入的Spliterator参数就是上面讲解的Spliterator实现类实例:

    public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
        Objects.requireNonNull(spliterator);
        return new ReferencePipeline.Head<>(spliterator,
                                            StreamOpFlag.fromCharacteristics(spliterator),
                                            parallel);
    }
    复制代码

    返回的实际上是一个ReferencePipeline.Head对象,我在上一个章节中有详细的讲解,现在我们再来分析一下加深印象。先看一下它的类继承关系:

    Head.png Head是ReferencePipeline的内部类,同时又继承了ReferencePipeline,ReferencePipeline是引用类型Stream的抽象,它实现了Stream接口,拥有中间操作和终止操作的能力,ReferencePipeline也继承了AbstractPipeline,这个抽象类上一章节有详细讲解。

    Head的构造函数调用ReferencePipeline构造函数:

    Head(Spliterator<?> source,
         int sourceFlags, boolean parallel) {
        super(source, sourceFlags, parallel);
    }
    

    ReferencePipeline(Spliterator<?> source,
    int sourceFlags, boolean parallel) {
    super(source, sourceFlags, parallel);
    }
    复制代码

    最终调用AbstractPipeline构造函数,上一章也有详细讲解,这里再回顾一下:

    AbstractPipeline(Spliterator<?> source,
                     int sourceFlags, boolean parallel) {
        //头结点没有前一个节点了
        this.previousStage = null;
        //代表源元素的Spliterator
        this.sourceSpliterator = source;
        //sourceStage变量指向自己
        this.sourceStage = this;
        this.sourceOrOpFlags = sourceFlags & StreamOpFlag.STREAM_MASK;
        // The following is an optimization of:
        // StreamOpFlag.combineOpFlags(sourceOrOpFlags, StreamOpFlag.INITIAL_OPS_VALUE);
        this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE;
        //深度为0
        this.depth = 0;
        //标识是不是并行流,默认为false
        this.parallel = parallel;
    }
    复制代码

    总结一下,通过StreamSupport#stream()传入Spliterator创建Stream,实际上返回的都是ReferencePipeline.Head对象,它代表源阶段的Stream,也是Pipeline链表的头结点。

    Spliterator调用时机

    前面分析了Spliterator和创建Stream的过程之后,还有一个疑问:Spliterator是在什么地方被使用到的呢?

    先说结论:Spliterator是在Stream调用终止操作的时候触发它的方法调用。其实这很容易理解,因为Stream是惰性流,创建和中间操作的时候什么都不会做,只有终止操作时才调用声明的处理数据的lambda表达式。

    看过上一章节的小伙伴应该还记得终止操作都会调用AbstractPipeline#evaluate()方法:

    final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
        assert getOutputShape() == terminalOp.inputShape();
        if (linkedOrConsumed)
            throw new IllegalStateException(MSG_STREAM_LINKED);
        linkedOrConsumed = true;
    
    return isParallel()
           ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
           : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
    

    }
    复制代码

    经过一系列调用,进入AbstractPipeline#copyInto()方法:

    final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
        Objects.requireNonNull(wrappedSink);
    
    //非短路操作
    if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
    
        //调用spliterato#getExactSizeIfKnown()获取源元素精确大小,然后传递给sink链表的begin()方法
      wrappedSink.begin(spliterator.getExactSizeIfKnown());
        //调用spliterator#forEachRemaining()批量遍历源元素
        spliterator.forEachRemaining(wrappedSink);
        wrappedSink.end();
    }
    //短路操作
    else {
        copyIntoWithCancel(wrappedSink, spliterator);
    }
    

    }
    复制代码

    终止操作是非短路操作的,在copyInto()中调用Spliterato#getExactSizeIfKnown()方法,会间接调用Spliterato#estimateSize(),然后调用Spliterato#forEachRemaining()批量遍历源元素。

    终止操作是短路操作的,会再调用AbstractPipeline#copyIntoWithCancel()方法:

    final <P_IN> void copyIntoWithCancel(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
        @SuppressWarnings({"rawtypes","unchecked"})
        AbstractPipeline p = AbstractPipeline.this;
        //取Pipeline链表头节点
        while (p.depth > 0) {
            p = p.previousStage;
        }
        wrappedSink.begin(spliterator.getExactSizeIfKnown());
        p.forEachWithCancel(spliterator, wrappedSink);
        wrappedSink.end();
    }
    复制代码

    ReferencePipeline#forEachWithCancel()方法:

    final void forEachWithCancel(Spliterator<P_OUT> spliterator, Sink<P_OUT> sink) {
        do { } while (!sink.cancellationRequested() && spliterator.tryAdvance(sink));
    }
    复制代码

    短路操作在do-while循环中调用tryAdvance()方法遍历Spliterator的单个源元素。

    总结

    本文首先介绍了Spliterator的含义,详细讲解了每一个方法的作用,然后通过ArraySpliterator和ArrayListSpliterator分析如何实现一个Spliterator接口,再次回顾了代表源Stream的Head类,最后分析了Spliterator方法的调用时机。

    写在最后

    • 阅读源码有什么用?

    Stream的数据处理逻辑都是通过lambda表达式定义的,它是一种声名式编程,与命令式编程不同。命令式编程比如传统的集合遍历迭代很好调试,而Stream很难调试,出了问题很多时候无从下手。通过阅读源码,了解原理,可以帮助我们更容易调试代码,定位问题。

    • 阅读建议

    本系列文章是以专栏的形式发布的,上下文多有关联,如果跳着阅读,可能会产生不连贯的感觉,所以建议按照顺序阅读。另外由于是源码分析,跟其他类型文章不同,所以建议在阅读的时候跟着思路亲自动手调试源码。

    最后,原创不易,如果觉得本系列文章对您有帮助,能够加深您对Stream原理和源码的理解的话,请不要吝啬您手中的赞(✪ω✪)!

    来源:https://juejin.cn/post/7101217470542774308
  • 相关阅读:
    【SQLServer 】SQLServer Alwayson环境在主库节点移除数据库
    Docker安装PostgreSQL
    基于海康监控的图像识别设计
    光线补偿算法的实现
    基于SURF特征的图像与视频拼接技术的研究和实现(一)
    opencv透视变换
    反人脸识别的思路和实现
    自动对比度的opencv实现
    单向信息传输系统设计实现
    集成利用tesseract.exe进行ocr
  • 原文地址:https://www.cnblogs.com/konglxblog/p/16476003.html
Copyright © 2020-2023  润新知