from 曾经对面的同事
每个Stream包含三个阶段 源、零个或多个中间操作、终止操作
源阶段
首先看一个例子,这里的例子非常简单,就是将List构造成了一个Stream,然后进行循环,
但是它是如何就将list构造成了Stream
List<String>stringList = new ArrayList<>();
stringList.add("颜智慧");
stringList.add("菜穗子");
stringList.stream().forEach(System.out::println);
点进stream()
方法
这是JDK1.8在Collection中新增加的默认方法,用于将集合构造成Strem对象,方法大体文档注释如下,构造关键在于spliterator()
方法
/**
* 返回一个以此集合为源的流对象
* 当这个方法无法返回IMMUTABLE和CONCURRENT特性值时应当被重写
* 默认会以集合的顺序创建流对象
* 默认串行
*/
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
spliterator()
方法也是Collection中新增加的默认方法,它重写了Iterable中的默认实现,创建了一个Spliterator对象,E代表集合中元素的类型
/**
* 以当前集合为源创建Spliterator对象
* Spliterator对象应当包含某些特性值,但是如果集合为空时就不需要包含SIZED特性值
* 这个方法应该被子类重写,应该具有不可变或是并发修改、延迟绑定、快速失败的特征,为了保证这个流与Collection中元素是等价的
* 空的spliterator()只会包含SIZED和SUBSIZED特性值
*/
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
可以看出Spliterator非常重要,它用于collection流源的构建,其实其它的Stream构造也是利用了Spliterator接口,我们来详细通读一遍Spliterator接口的文档和方法说明;
Spliterator 分割迭代器
下面是Spliterator接口中的接口和一些特性值,方法描述我以注释的形式标识出来
package com.tinny.jdk8.spliterator;
import java.util.Comparator;
import java.util.function.Consumer;
/**
* 名词解释:
* Spliterator 以下简称Sp
* structural interference 译为:结构被修改,也就是数据源被修改
* <p>
* Spliterator文档解析描述
* <p>
* 它是一个用于分区和遍历源数据的对象,源可以是数组、集合或者是io函数;
* <p>
* 可以通过tryAdvance单独遍历或是forEachRemaining循环遍历;
* <p>
* 可以将一个Sp分裂成两个平行的Sp,用于并行计算,如果分裂代价过大或者分裂没有意义对并行来说就是负担,
* 每个Sp只会作用到属于自己的那一块区域;
* <p>
* 它有一些特性值,例如ORDERED、DISTINCT等,用于简化计算,类似Collectors中特性值含义,
* 例如来自List的Sp就会包含ORDERED,Set就会包含DISTINCT等;
* <p>
* 某些特性值会有特殊含义,例如ORDERED就代表会被顺序执行,JDK告诉我们不要去定义新的特性,老老实实用它现在的就行了;
* <p>
* <p>
* 如果一个Sp不包含IMMUTABLE或者CONCURRENT,会在绑定之后进行源数据的检测,
* 一个延迟绑定的Sp绑定数据源出现方法的第一次遍历、分割或是第一次查询大小,而不是第一次创建,
* 一个非延迟绑定的Sp,绑定数据在于构造或是任意方法第一次调用,
* 如果绑定之前修改的数据源,那么绑定后的数据也会被改变,但是如果绑定之后检测到数据结构被修改,就会抛出ConcurrentModificationException异常,
* Sp有快速失效策略,例如forEachRemaining方法,Sp会优化遍历策略,并不是在每次调用元素时就会检测一次,这样效率比较低,它是在所有元素执行完毕之后再进行检测;
* <p>
* Sp提供一个估值的操作,如果包含SIZED特性,这个估值就是代表所有遇到的元素,如果不包含也会提供一个大概的值用于分割;
* <p>
* 虽然Sp分裂非常适合于并行操作,但是它不是线程安全的,需要由调用者来保证,通常都是使用顺序调用的方式来避免线程安全问题,Sp分裂后可以进行再次分裂,
* 分裂最好都是在元素被消费之前进行分裂,因为元素被消费后,分裂出对应的SIZED往往是不准确的;
* <p>
* 为了提高效率,减少装箱拆箱的损耗,提供了OfInt、OfLong、OfDouble的特例,具体细节可以看代码;
* <p>
* Sp类似于Iterator,用来进行元素遍历,它提供了效率更高的并行分割遍历的方式,抛弃了Iterator的hasNext()和next()方法,避免线程竞争,
* Iterator会通过两次校对hasNext()和next()获取下一个元素,Sp将其简化为一个方法,具体哪个方法我也没细看。。TODO Sp的简化遍历的方法;
* <p>
* 对于可变源,如果在源绑定到消费这个过程中,这个过程中源结构如果发生变化(替换、新增、删除),结果就会具备不确定性;
* <p>
* 可变源可以通过如下几种方式来控制
* 1、源结构不能被修改 CopyOnWriteArrayList是一个不可变的源,通过复制重写的方式来实现源结构更新,通过这种方式避免了线程竞争的方式,适合于读多写少的场景;
* 2、可以并发修改的源 ConcurrentHashMap通过分段锁来控制每一片bucket;
* 3、延时绑定和快速失败的源 ArrayList以及大部分的Collection的子类都具备这个特性,会在源绑定之后元素消费之前如果检测到结构被修改,会遵循快速失效的策略;
* 4、非延时绑定和快速失败的源 类似3,但是由于它绑定时机更早,所有从元素绑定到检测时间会更长
* 5、延迟绑定和非快速失败的源 在绑定后进行遍历(消费)中,如果元素发生的变化,由于没有快速失败策略,后续的行为是不确定的;
* 6、非延时绑定和非快速失败的源 在构造或是某个方法调用时就会被绑定,中间元素改变后,后续的行为也是不确定的;5,6两种都是有风险的
* <p>
* 下面有两个例子介绍了Sp和并行分割基本用法,细节可以自己去看
*/
public interface Spliterator<T> {
/**
* 尝试获取元素
* 如果元素存在,就会执行给定操作,并返回true;元素不存在,就会返回false;
* 异常会返回给调用者
*
* @param action 给定动作
*/
boolean tryAdvance(Consumer<? super T> action);
/**
* 对于每个遇到的元素会在当前线程执行给定的动作,也就是循环;
* 在所有元素消费完或是抛出异常后停止;
*
* @param action 给定动作
*/
default void forEachRemaining(Consumer<? super T> action) {
do {} while (tryAdvance(action));
}
/**
* 尝试分割
* 如果当前Sp可以被分割,当前的Sp会被截取,返回一个全新的sp并持有一定数量的元素;
* 如果当前Sp包含ORDERED,返回的元素也必须包含;
* 除非当前Sp是一个无限流,否则重复调用trySplit()最终会返回一个null;
* 拆分前的estimateSize,必须大于或是等于拆分后Sp的estimateSize;
* 如果这个sp包含SUBSIZED,那么分裂前的estimateSize必须等于分裂后estimateSize的总和;
* 可能会因为任意原因返回null,本身就是一个空值、已经是最小分割单元等等原因,提醒我们要做校验;
*
* @return New Sp/null
* @