上一篇文章查看了jdk关于Stream流的创建,其中用于数据保存的便是Spliterator,这篇文章将会对Spliterator的源码进行查看,来看看在流中的数据是如何存储的。
Spliterator是一个接口,它拥有子接口
Spliterator.OfDouble, Spliterator.OfInt, Spliterator.OfLong, Spliterator.OfPrimitive<T,T_CONS,T_SPLITR>
这些子接口通过源码可以发现是对Spliterator的继承
public interface OfPrimitive> extends Spliterator { /* other code */}复制代码
这就是嵌套接口。这几个子类通过继承来扩展接口。在《thinking in java》一书对这个方法的介绍:
通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。这两种情况都可以获得新的接口。
而上面的代码就是对多个接口进行继承产生新的接口。
所有对Spliterator的实现类:
Spliterators.AbstractDoubleSpliterator, Spliterators.AbstractIntSpliterator, Spliterators.AbstractLongSpliterator, Spliterators.AbstractSpliterator
这里发现所有Spliterator的实现类都是位于Spliterators下面的内部类,那么可以发现它的设计如下:
定义一个接口A,而对该接口的扩展在它内部实现。而对该接口的实现则创建对应的类As中的内部类进行实现
在文档中对于该类作用的主要作用介绍为:
用于遍历和分区源元素的对象。 Spliterator所涵盖的元素源可以是例如数组,集合,IO通道或生成器函数。并且还能对其某些元素进行拆分使得可以采用并行操作(这里我觉的和分支-合并框架有点类似)。 此外还增加了一组特征值表示ORDERED, DISTINCT, SORTED, SIZED, NONNULL, IMMUTABLE, CONCURRENT, SUBSIZED.Spliterator客户端可以使用这些来控制,专门化或简化计算。例如,Collection的Spliterator将报告SIZED,Set的Spliterator将报告DISTINCT,而SortedSet的Spliterator也会报告SORTED。特征报告为简单的联合位集。一些特征还限制了方法行为;例如,如果ORDERED,遍历方法必须符合其记录的顺序。未来可能会定义新特征,因此实现者不应将意义分配给未列出的值。看起来很想Iterator,但他比Iterator效率更高,适用范围更广。与迭代器一样,Spliterators用于遍历源的元素。 除了顺序遍历之外,Spliterator API还支持有效的并行遍历,支持分解以及单元素迭代。 此外,通过Spliterator访问元素的协议旨在实现比Iterator更小的每元素开销,并避免使用hasNext()和next()的单独方法所涉及的固有竞争。
定义的方法
- int characteristics() - 返回Spliterator数据对应的特征值。根据文档给出的事例,可知它返回的是多个特征码的按位或的结果。
public int characteristics() { return ORDERED | SIZED | IMMUTABLE | SUBSIZED;}复制代码
- default void forEachRemaining(Consumer<? super T> action)方法 - 在当前线程中按照给定的操作对所有元素执行一遍
- long estimateSize() - 返回forEachRemaining(Consumer<? super T> action)在执行前将要遇到的元素数量的估算值,如果太高就返回Integer.MAX_VALUE。
- boolean tryAdvance(Consumer<? super T> action) - 如果存在剩余元素,则对其执行给定的操作
这里和第一个方法并不相同,根据文档给出的事例,该方法事例实现如下:
public boolean tryAdvance(Consumer action) { if (origin < fence) { action.accept((T) array[origin]); origin++; return true; } else { return false; }}/*其中array为Object[]用来存储数据,origin为开始执行的下标,fence为结束执行的下标。从中我们可以看到该方法只执行一次,如果成功就返回true否则返回false.*/复制代码
- Spliterator trySplit() - 如果可以对此spliterator进行分区,则返回Spliterator覆盖元素,这些元素在从此方法返回时将不被此Spliterator覆盖。这样看起来有点难懂,关于这个方法其实就是负责并行的方法,根据文档原文:A Spliterator may also partition off some of its elements (using trySplit()) as another Spliterator, to be used in possibly-parallel operations. 可知,该方法用我的理解就是对该对象的元素进行切分,用切分出来的部分创建一个新的Spliteraor对象以方便进行并行操作.而调用该方法的线程会将返回的Spliterator交给另一个新的线程,新的线程又可以继续分区。这样使得程序的执行速度大大提高。如果没有公共参数的话还能完全不考虑死锁这种资源占用的问题。
对于该分成多少个线程这点,如果线程池中线程数量过多,最终他们会竞争稀缺的处理器和内存资源,浪费大量的时间在上下文切换上。反之,如果线程的数目过少,那么处理器的一些核可能无法充分利用。关于这点,在一书中Brian Goetz提出的解决办法:
线程池大小与处理器的利用率纸币可以使用下面的公式进行估算:
N(threads) = N(cpu) * U(cpu) * (1 + W/C) 其中:
- N(cpu)是处理器的核的数目,可以通过Runtime.getRuntime().availableProcessors()得到
- U(cpu)是期望的CPU利用率(该值应该介于0到1之间)
- W/C是等待时间与计算时间的比率 例:如果应用99%的时间都在等待请求的相应,所以估算出来的W/C比率为100
- default Comparator<? super T> getComparator() - 如果此Spliterator的源由比较器SORTED,则返回Comparator。而上文说了当数据结构为SortedSet的时候比较器会为SORTED。
- default long getExactSizeIfKnown() - 如果此Spliterator为SIZED则返回estimateSize()的便捷方法,否则为-1。当比较器为SIZED的时候数据源为Collection,为有序的集合,此时该方法的作用与estimateSize()相同.而源码也是如此:
default long getExactSizeIfKnown() { return (characteristics() & SIZED) == 0 ? -1L : estimateSize();}复制代码
- default boolean hasCharacteristics(int characteristics) - 如果此Spliterator的特性包含所有给定特征,则返回true。
default boolean hasCharacteristics(int characteristics) { return (characteristics() & characteristics) == characteristics;}复制代码
从给出的方法来看Spliterator这个接口的主要功能是存储数据以及更加方便地并行处理数据,他把后续对数据的操作通过函数分离到了外部。这种设计好处显而易见了,我们可以对数据前后进行多次不同的操作。这既视感像什么呢?没错,就是
Arrays.asList(1, 2, 3).stream() .filter(i -> i % 2 == 0) .sort(Integer::compare) .collect(Collectors.toList());复制代码
这样的filter和sort之类的一系列操作都可以实现了。
Spliterator子接口
- static interface Spliterator.OfDouble - 浮点数专用Spliterator
- static interface Spliterator.OfInt - 整数专用Spliterator
- static interface Spliterator.OfLong - 长整型数专用Spliterator
- static interface Spliterator.OfPrimitive<T,T_CONS,T_SPLITR extends Spliterator.OfPrimitive<T,T_CONS,T_SPLITR>> - 专门用于原始值的Spliterator
以上几个子接口是Spliterator为基础数据类型提供的原始子类型特化,其目的是为了避免java装箱操作所产生的多余损耗。
装箱操作 - 当java中基本数据类型和转为其对应的包装类型的时候就是装箱操作。但是会产生多余的运行损耗.如int转为Integer
原始类型特化 - 函数接口java.util.function.Consumer的类型特例化,也就是限定accept参数类型.例如int的原始类型特里化为java.util.function.IntConsumer其accept的参数为int
其中对参数为Consumer<T>类型的方法重载,以及覆盖。具体例子如下:
@Overridedefault boolean tryAdvance(Consumer action) { if (action instanceof IntConsumer) { return tryAdvance((IntConsumer) action); } else { if (Tripwire.ENABLED) Tripwire.trip(getClass(), "{0} calling Spliterator.OfInt.tryAdvance((IntConsumer) action::accept)"); return tryAdvance((IntConsumer) action::accept); }}复制代码
其中Tripwire.ENABLED用于检测java类中无意中使用装箱的实用程序类。根据是否开启系统属性org.openjdk.java.util.stream.tripwire来检测。通常情况下是关闭的。 具体代码如下:
private static final String TRIPWIRE_PROPERTY = "org.openjdk.java.util.stream.tripwire";/** Should debugging checks be enabled? */static final boolean ENABLED = AccessController.doPrivileged( (PrivilegedAction) () -> Boolean.getBoolean(TRIPWIRE_PROPERTY));复制代码
doPrivileged(PrivilegedAction action);接受的是函数接口。 但是它本身使用native修饰的,也就是它的实现位于其他语言中.
@CallerSensitivepublic static nativeT doPrivileged(PrivilegedAction action);复制代码
而它参数的结构如下:
public interface PrivilegedAction{ T run();}复制代码
回到正题,boolean tryAdvance(Consumer<? super Integer> action) action)这个方法的具体流程是先判断传入的Lambda表达式是否为原始类型特化类型,如果是则直接调用boolean tryAdvance(IntConsumer action)方法。否则检测org.openjdk.java.util.stream.tripwire是否开启,如果开启则生成日志警告:{0} calling Spliterator.OfInt.tryAdvance((IntConsumer) action::accept),否则将Lambda强制转化为原始类型特化类型并使用::创建方法引用。
总结
Spliterator作为java1.8新加入的对数据进行操作的类,它完全利用了函数式编程的便利性,将数据的具体操作分离出去,大大提高了灵活性。同时采用类似于分支-合并的结构,为数据操作提供了并行的可能,提升了执行速度。和Iterator相比,Spliterator并不能直接将数据传递给我们。它只会执行我们给定的操作,同时它的遍历是在其内部进行,不需要我们再去编写for循环。