JAVA8学习10-spliterator 接口分析

10. spliterator 分割迭代器

​ spliterator 是一个可分割迭代器(splitable iterator),可以和 iterator 顺序遍历迭代器一起看。jdk1.8发布后,对于并行处理的能力大大增强,Spliterator 就是为了并行遍历元素而设计的一个迭代器,jdk1.8中的集合框架中的数据结构都默认实现了 spliterator

​ 分割迭代器主要是用来对源数据元素进行遍历和分区。分割迭代器的源数据可以是数组、集合、IO通道以及生成器函数(比如Stream的iterate方法)。
 分割迭代器遍历元素有两种方式:

  1. 通过 tryAdvance 方法单个元素的遍历。
  2. 通过 forEachRemaining 方法顺序的按块遍历。

​ 一个分割迭代器可以调用 trySplit 方法,分割元素成为另一个分割迭代器。在某些情况下使用并行的分割迭代器的操作可能效果并不是太好,比如元素不能再分割,分割的元素数量不平衡,分割的效率比较低等。每一个分割迭代器只对本块的元素有用。
 每一个分割迭代器的结构、源数据、元素有不同的特征,比如ORDERED、DISTINCT、SORTED、SIZED、NONNULL、IMMUTABLE、CONCURRENT、SUBSIZED。这些特征值主要是便于客户端的控制、特化和简化计算。比如Collection的分割迭代器会有SIZED这一特征,Set的分割迭代器会有DISTINCT这一特征值等。

10.1 特性值说明

// 表明元素是有序的,如果是此特征值,那么trySplit分割之后的元素是有序的,tryAdvance处理元素也是有序的,forEachRemaining处理元素也是有序的。如果Collection#iterator是有序的,那么Collection也是有序的。List的遍历顺序是按照索引顺序,HashSet是无序的
public static final int ORDERED    = 0x00000010;
// 表明元素是去重的
public static final int DISTINCT   = 0x00000001;
// 表明遍历的顺序是排好序的,再此特征值下getComparator将会返回相关联的比较器或者null(自然排序),如果包含了此特征值,那么分割迭代器必然是ORDERED的
public static final int SORTED     = 0x00000004;
// 表明在遍历和分割之前 estimateSize 的返回值是一个有限的值,并且在没有源结构修改的情况下(增加、删除等),返回值将是遍历元素个数的准确值 这里需要看下 estimateSize 方法介绍
public static final int SIZED      = 0x00000040;
// 表明元素是非空的
public static final int NONNULL    = 0x00000100;
// 表示元素源不能进行结构修改的特征值;也就是说,无法添加,替换或删除元素,因此在遍历期间不会发生此类更改。不报告{IMMUTABLE}或{CONCURRENT}的Spliterator应该有关于遍历期间检测到的结构干扰的文档化策略(例如抛出{@link ConcurrentModificationException})。
public static final int IMMUTABLE  = 0x00000400;
//表示可以在没有外部同步的情况下由多个线程安全地同时修改元素源(允许添加,替换和/或删除)的特征值。如果是这样,Spliterator应该有一个关于遍历期间修改影响的书面政策。顶层Spliterator不应该同时返回{CONCURRENT}和{SIZED},因为如果已知,有限大小可能会在遍历期间同时修改源时发生更改。这样的Spliterator是不一致的,并且不能保证使用该Spliterator进行任何计算。如果子拆分大小已知,则子拆分器可以报告{@code SIZED},并且在遍历时不反映对源的添加或删除。大多数并发集合维护一致性策略,保证Spliterator构造点上存在的元素的准确性,但可能不反映后续的添加或删除。
public static final int CONCURRENT = 0x00001000;
// trySplit 返回的子分割迭代器都会是SIZED和SUBSIZED的。 分割成子,并且子的部分是固定大小的
public static final int SUBSIZED   = 0x00004000;

​ 特性值是用来固定化或简化计算。相当于 collect 的 indetify 包含的情况是下是直接将中间结果强转为返回结果类型的,不包含情况下,才会执行 finisher 函数。
如果分割迭代器没有包含 IMMUTABLE 和 CONCURRENT 特征值,那么分割迭代器就需要关注:分割迭代器何时绑定数据源、数据源绑定后的数据源发生结构的变化(比如添加元素、删除元素)。如果是延迟绑定的分割迭代器,那么迭代器在第一次遍历,或第一次分割、或第一次查询大小时,绑定数据源,而不是在分割迭代器创建的时候。如果分割迭代器不是延迟绑定的,那么它绑定数据源的时刻是分割迭代器创建的时候或者调用任意的方法时。如果对数据源的修改先于绑定行为,那么分割迭代器遍历的时候会反应出此变化(比如添加的元素会被遍历)。如果修改是在绑定行为之后,就会抛出ConcurrentModificationException异常(就是对同个集合同时执行遍历和插入或修改操作产生的异常)。这种行为就是fail-fast(快速失败) 。块遍历的方法(forEachRemaining)可能会优化遍历,并且在元素遍历之后会检查结构变化,而不是检查每一个元素(马上报告异常)。
 分割迭代器可以调用 estimateSize 方法估算出保持的元素数量,理想状态下,如果分割迭代器含有 SIZED 这一特征值,estimateSize 的返回值严格相等待遍历的元素的数量。但是呢,也存在不相等的情况,虽然不相等,但是估计值依然是有用的,比如是否需要进一步分割等。
 尽管分割迭代器在并行计算中有明显的效用,分割迭代器并不是线程安全的。使用分割迭代器实现并行算法应该确保分割迭代器在同一时刻只能被一个线程使用。这种实现比较容易的方案是 serial thread confinement(串行线程封闭)(通过将多个并发的任务存入队列实现任务的串行化,并为这些串行化的任务创建唯一的一个工作线程进行处理),调用 trySplit 方法的线程传递返回的 Spliterator 给另一个线程(可以遍历或者进一步分割迭代器),假如有两个或者更多的线程并行的操作,那么到底是遍历还是分割将是不确定的。假如一个线程分割,另一个线程计算分割的结果,最理想的状态是线程切换发生在元素被 consume 之前(也就是tryAdvance 方法调用之前)。
 JDK为原始类型提供了特化的实现,譬如ofInt、ofLong等,这种方式避免了拆装箱带来的性能消耗。
 像迭代器一样,分割迭代器用于遍历元素,只不过分割迭代器还以分区的形式支持并行遍历。与迭代器相比,分割迭代器访问元素的消耗更小,因为分割迭代器避免了潜在的竞争(普通迭代器访问元素需要hasNext方法和next方法配合,分割迭代器只需一个tryAdvance方法,将两个步骤原子化)
 对于可变的数据源,假如在绑定数据与遍历结束期间,源数据发生了结构的改变(增加、删除等),那么可能会产生无法预料的结果。这就是 ConcurrentModificationException 异常产生的原因。
 结构上的改变也可以通过下列的方法管理:
  源数据不能被结构地改变,比如数据源是CopyOnWriteArrayList(不变的源,适用于读多写少的操作的集合)的实例,那么与之绑定的分割迭代器需配置IMMUTABLE 特征值。
  源数据管理并行的修改,ConcurrentHashMap的key的Set是源数据,分割迭代器需配置 CONCURRENT 特征值。
  可变的源数据提供延迟绑定和 fail-fast 分割迭代器。
延迟绑定缩短了窗口时间(创建分割迭代器到元素遍历的时间),fail-fast 可以在遍历开始后检测结构改变,并且抛出异常。
  可变源提供了非延迟绑但是fail-fast分割迭代器,这样源就增加了抛出异常的可能性(ConcurrentModificationException)。
  可变源提供了延迟绑定但是non-fail-fast分割迭代器,这样源就增加了未知结果的可能性。
  可变源提供了非延迟绑定和non-fail-fast分割迭代器,这样源就增加了未知结果的可能性,因为可能发生结构变化

10.2 方法说明

10.2.1 boolean tryAdvance(Consumer<? super T> action) 单个对下一个元素执行给定的动作,如果有剩下元素未处理,执行则返回true,否则返回false

​ 如果分割迭代器存在元素,那么就执行给定的action,并且返回true,否则返回false。如果分割迭代器是ORDERED的,那么元素也会顺序的执行action。被action发出的异常会传递给调用者。

10.2.2 default void forEachRemaining(Consumer<? super T> action) 对每个剩余元素执行给定的动作,依次处理,直到所有元素已被处理或被异常终止。默认方法调用tryAdvance方法。

每一个剩余的元素在当前线程中串行地执行给定的action,如果分割迭代器是ORDERED 的,那么元素也会顺序的执行action。被 action 发出的异常会传递给调用者。

10.2.3 Spliterator trySplit() 对任务分割,返回一个新的Spliterator迭代器。

​ 用于分割迭代器的分割。假如分割迭代器(母)可以被分割,调用该方法就会返回一个新的分割迭代器(子)。

母分割迭代器的元素是
    1,2,3,4,5,6,7,8
分割之后
母分割迭代器的元素是 5,6,7,8
子分割迭代器的元素是 1,2,3,4

如果母分割迭代器是 ORDERED 的,那么调用该方法的子分割迭代器也是ORDERED 的。
  重复调用该方法,最终一定会返回null,除非母分割迭代器的元素是无限的。如果调用该方法的返回值为非空的(也就是有子分割迭代器),那么在调用此方法分割之前调用 estimateSize 的值必然会大于或等于分割之后母分割迭代器与子分割迭代器的 estimateSize 值。除此之外,如果母分割迭代器的特征值是SUBSIZED的,那么在分割之前的 estimateSize 的值必须等于分割之后母分割迭代器estimateSize 和子分割迭代器 estimateSize的 和。

分割之前(trySplit之前)母分割迭代器的元素
    1,2,3,4,5,6,7,8
一定大于或等于分割之后母分割迭代器、子分割迭代器的元素个数

分割之前(trySplit之前)母分割迭代器的元素
    1,2,3,4,5,6,7,8
分割之后 母分割迭代器的元素1,2,3,4  子分割迭代器的元素5,6,7,8 一定等于母子元素个数之和

如果分割迭代器的元素为空、遍历已经开始、数据结构的限制、效率的限制,以上情况可能该方法会返回空
理想情况调用 trySplit 分割,会将元素分为五五开。非理想情况可能保持很好的效用,比如树结构。但是不平衡或者低效的分割会导致并行的低效率

10.2.4 long estimateSize() 用于估算还剩下多少个元素需要遍历

计算出分割迭代器的元素个数的估算值。返回的值有两种情况,一种情况就是forEachRemaining将要遍历到的元素的个数,另一种情况就是Long#MAX_VALUE(元素无穷的、计算成本较高)。

在以下两种情况,该方法的返回值就是分割迭代器元素的个数。

  1. 分割迭代器是 SIZED,并且还没开始分割和遍历
  2. 分割迭代器是 SUBSIZED 的,并且还没开始遍历

​ 除了以上两种情况,返回值可能是非正确的,但是该值会随着trySplit的调用而减少。
​ 非精确的估算值也是有用的,并且计算也是很方便的,比如一个平衡的二叉树的子分割迭代器可能会返回其母分割迭代器的元素个数的一半,如果母分割迭代器保持的元素个数是不精确的,子分割迭代器可能就是与树的深度有关。

10.2.5 int characteristics() 返回当前对象有哪些特征值

​ 返回分割迭代器的特征值,在分割迭代器之前,重复调用该方法的返回值是一样的,分割之前的特征可能会与分割之后的特征不同。

10.2.6 default long getExactSizeIfKnown() 当迭代器拥有 SIZED 特征时,返回剩余元素个数;否则返回-1

​ 默认实现方法是 estimateSize()

10.2.7 default boolean hasCharacteristics(int characteristics) 是否具有当前特征值
10.2.8 default Comparator<? super T> getComparator() 返回当前分割器的比较器

​ 如果 Spliterator的 源数据 是通过 Comparator 排序的,则返回 Comparator,如果 Spliterator 的 源数据 是自然排序的 ,则返回null,其他情况下抛错

10.3 衍生接口

10.3.1 OfPrimitive 专门用于原始值(基本数据类型)的 Spliterator
public interface OfPrimitive<T, T_CONS, T_SPLITR extends Spliterator.OfPrimitive<T, T_CONS, T_SPLITR>>
        extends Spliterator<T> {
    @Override
    T_SPLITR trySplit();
    @SuppressWarnings("overloads")
    boolean tryAdvance(T_CONS action);
    @SuppressWarnings("overloads")
    default void forEachRemaining(T_CONS action) {
        do { } while (tryAdvance(action));
    }
}

​ 参数 T 是返回的类型,必须是基本类型的包装类型,
​ 参数 T_CONS 原始的 consumer 类型,必须是原始特化的消费类型,如 IntConsumer
​ 参数 T_SPLITR 原始 Spliterator 的类型。该类型必须是{@code T}的Spliterator的原始特化, 如 Spliterator.OfInt

​ 可以看到,这个接口基本没有变动,这是多增加两个泛型声明而已,本质上和Spliterator没有太大的区别,只不过,它限制tryAdvance的参数action类型T_CONS和trySplit的返回参数T_SPLITR必须在实现接口时先声明类型。

​ 基于OfPrimitive接口,又衍生出了OfInt、OfLong、OfDouble等专门用来处理int、Long、double等分割迭代器接口(在Spliterators有具体的实现)

10.3.2 OfInt 专门用于 int 值的 Spliterator。
public interface OfInt extends OfPrimitive<Integer, IntConsumer, OfInt> {

    @Override
    OfInt trySplit();

    @Override
    boolean tryAdvance(IntConsumer action);

    @Override
    default void forEachRemaining(IntConsumer action) {
        do { } while (tryAdvance(action));
    }
  // 如果该操作是 IntConsumer的实例,则将其转换为 IntConsumer 并传递给 tryAdvance(IntConsumer);否则,通过装箱 IntConsumer的参数,该动作适用于 IntConsumer 的实例,然后传递给tryAdvance(IntConsumer)。
    @Override
    default boolean tryAdvance(Consumer 和<? super Integer> action) {
        if (action instanceof IntConsumer) {
          // Consumer 和 IntConsumer 接口之间是没有任何的继承关系的,为什么能够强转成功
          //1. Consumer 本身接收一个参数类型不返回结果,IntConsument 是接收一个int 类型,不返回结果,当 两者接收参数是 int 时,是等价的。
          //2. 函数式编程 lamdba 表达式 相当于一个实例,是同个上下文关系推算类型,同一个 lambda 表达式,可以表示多种类型函数
            return tryAdvance((IntConsumer) action);
        }
        else {
            if (Tripwire.ENABLED)
                Tripwire.trip(getClass(),
                              "{0} calling Spliterator.OfInt.tryAdvance((IntConsumer) action::accept)");
          // 这里比较简单理解,就是 consumer 的 accpet方法引用到 IntConsumer 的 accpet 上
            return tryAdvance((IntConsumer) action::accept);
        }
    }

    @Override
    default void forEachRemaining(Consumer<? super Integer> action) {
        if (action instanceof IntConsumer) {
            forEachRemaining((IntConsumer) action);
        }
        else {
            if (Tripwire.ENABLED)
                Tripwire.trip(getClass(),
                              "{0} calling Spliterator.OfInt.forEachRemaining((IntConsumer) action::accept)");
            forEachRemaining((IntConsumer) action::accept);
        }
    }
}

介绍下intConsumer 和 consumer 强转的例子:

package cn.zxhysy.jdk8.stream2;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.IntConsumer;

public class StreamTest3 {

    public static void main(String[] args) {
//        Consumer<Integer> consumer = (val) -> {
//            System.out.println(val);
//        };
//        这种写法报错 java.lang.ClassCastException
//        IntConsumer intConsumer =(IntConsumer) consumer;
//        intConsumer.accept(10);
        // 这种可以执行
//        IntConsumer intConsumer =  (IntConsumer) consumer::accept;
//        intConsumer.accept(20);

        StreamTest3  streamTest3 = new StreamTest3();
        Consumer<Integer> consumer = val -> System.out.println(val);
        IntConsumer intConsumer = val -> System.out.println(val);

        streamTest3.test(consumer);// 传统面向对象方式 200
        streamTest3.test(consumer::accept);// 函数式方式 200
        streamTest3.test(intConsumer::accept);//200
    }

    public void test(Consumer<Integer> consumer){
        if(consumer instanceof IntConsumer){
            // 这里不知道要怎么调用才会进来
            System.out.print("true:");
            consumer.accept(100);
        } else {
            System.out.print("false:");
            consumer.accept(200);
        }

    }

}

参考地址:https://blog.csdn.net/lh513828570/article/details/56673804

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值