前言

stream是怎么做到一次迭代中将所有流操作进行叠加?stream怎么做到只有在终止操作时进行元素遍历?那中间操作是做了些什么?

Stream 与集合的区别

  • 集合是内存中的数据结构抽象,描述了数据在内存中是如何存储的。
  • 流描述了对数据处理的过程,是一系列运算操作的叠加。对 stream 的任何修改都不会修改背后的数据源,比如对 stream 执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新 stream。

流可以是无限流,而集合不能是无限集合。

Stream 生成

工厂方法

Stream integerStream = Stream.of(1, 2, 3, 5);

Stream.generate(Math::random);

Stream.iterate(1, item -> item > 10, item -> item + 1)

集合转化

Collection 接口有一个 stream(),所以其所有子类都可以通过该方法获取相应的 Stream 对象。

Map<String, Double> nameToScore = new HashMap<>();
double totalScore = nameToScore.entrySet().mapToDouble(Entry::getValue).sum();

数组转化

int temperature = {21, 23, 27};
int sum = Arrays.stream(temperature).sum();
// int sum = Arrays.stream(temperature).reduce(Integer::sum).orElse(0);

使用

对 stream 的操作分为为两类,中间操作和结束操作:

  • 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新 stream。中间操作又可以分为无状态的和有状态的。
  • 无状态中间操作是指元素的处理不受前面元素的影响。
  • 而有状态的中间操作必须等到所有元素处理之后才知道最终结果。
  • 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以 pipeline 的方式执行,这样可以减少迭代次数。计算完成之后 stream 就会失效。结束操作又可以分为短路操作和非短路操作。
  • 短路操作是指不用处理全部元素就可以返回结果,比如找到第一个满足条件的元素。

java stream处理效率_java

优点

流的优点是可以通过组合语义明确的 operator,使最终的代码更加简洁且可读性更强。

通过一个例子来看流与非流的实现的区别:从一个字符串列表中找到以 a 开头、最长的字符串长度。

非 Stream 实现

List<String> strings = Arrays.asList("a", "bb", "ccc", "abcd");

List<String> startWithAList = new ArrayList<>();
for (String string: strings) {
    if(string.startsWith("a")){
        startWithAList.add(string); // 1. filter(), 保留以A开头的字符串
    }
}

List<Integer> lengths = new ArrayList<>();
for (String string : startWithAList) {
    lengths.add(string.length());   // 2. mapToInt(), 转换成长度
}

int maxLength = 0;
for(Integer length : lengths){
    maxLength = Math.max(length, maxLength);   // 3. max(), 保留最长的长度
}

System.out.println(String.format("the maxLength of %s is %d", strings, maxLength));

这中实现方式有两个明显的弊端:

  • 对原始数据迭代次数过多
  • 需要对迭代过程中产生的中间结果进行额外存储

更好的实现应该为:

List<String> strings = Arrays.asList("a", "bb", "ccc", "abcd");

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

System.out.println(String.format("the maxLength of %s is %d", strings, maxLength));

这其实就是 stream 内部实际的执行过程,这是 stream 的作者抽象出了丰富的 operator,并构建好了这些 operator 的通用流程,只将 operator 中与业务逻辑有关的操作通过接口的形式(lambda 也是一种接口)暴露给程序员,从而增强了代码的复用性的可读性。

Stream 实现

List<String> strings = Arrays.asList("a", "bb", "ccc", "abcd");
int maxLength = strings.stream()
					.filter(s -> s.startsWith('a'))
					.mapToInt(String::length())
					.max().orElse(0);
					
System.out.println(String.format("the maxLength of %s is %d", strings, maxLength));

Stream 原理

java stream处理效率_java stream处理效率_02

AbstractPipeline

上图通过 Stream.of 方法得到 Head Stage,紧接着调用一系列的中间操作,不断产生新的 stream,这些 Stream 对象以双向链表(AbstractPipeline#previousStage、AbstractPipeline#nextStage)的形式组织在一起,构成整个流水线。

Sink

  • An extension of {@link Consumer} used to conduct values through the stages of a stream pipeline, with additional methods to manage size information, control flow, etc. Before calling the {@code accept()} method on a {@code Sink} for the first time, you must first call the {@code begin()} method to inform it that data is coming (optionally informing the sink how much data is coming), and after all data has been sent, you must call the {@code end()} method. After calling {@code end()}, you should not call {@code accept()} without again calling {@code begin()}. {@code Sink} also offers a mechanism by which the sink can cooperatively signal that it does not wish to receive any more data (the {@code cancellationRequested()} method), which a source can poll before sending more data to the {@code Sink}.

    A stream pipeline consists of a source, zero or more intermediate stages (such as filtering or mapping), and a terminal stage, such as reduction or for-each.

    A Sink instance is used to represent each stage of this pipeline , The Sink implementations associated with a given stage is expected to know the data type for the next stage, and call the correct {@code accept} method on its downstream {@code Sink}. Similarly, each stage must implement the correct {@code accept} method corresponding to the data type it accepts.

如果说 Pipeline 是为了将对源数据以 stage 的形式将操作串联起来,那么 Sink 就是为了封装对源数据的操作。前一个stage 只需调用后一个 stage 的 accept() 方法即可,并不需要知道其内部是如何处理的。

  • 当然对于有状态的操作,Sink#begin() 和 Sink#end() 方法也是必须实现的。比如 Stream#sorted() 是一个有状态的中间操作,其对应的 Sink#begin() 方法会创建一个临时容器,而 Sink#accept() 方法负责将元素添加到该容器中, Sink#end() 则负责对容器进行排序。
  • 对于短路操作,Sink#cancellationRequested() 也是必须实现的。比如 Stream.findFirst() ,只要找到一个元素,cancellationRequested() 就应该返回 true,以便调用者尽快结束查找。

Sink 的四个接口方法常常相互协作,共同完成计算任务。实际上 Stream API 内部实现的的本质,就是如何重载 Sink 的这四个接口方法。

下面我们结合具体例子看看 Stream 的中间操作是如何将自身的操作包装成 Sink 以及 Sink 是如何将处理结果转发给下一个 Sink 的。先看 Stream.map() 方法:

// java.util.stream.SortedOps.RefSortingSink:371行
// Stream.sorted()方法用到的Sink实现,由java.util.stream.SortedOps.OfRef#opWrapSink:133行 触发
class RefSortingSink<T> extends AbstractRefSortingSink<T> {
    private ArrayList<T> list;// 存放用于排序的元素
    RefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
        super(downstream, comparator);
    }
    @Override
    public void begin(long size) {
        ...
        // 1. 创建一个存放排序元素的列表
        list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
    }
    @Override
    public void end() {
        list.sort(comparator);// 3. 只有元素全部接收之后才能开始排序
        downstream.begin(list.size());
        if (!cancellationWasRequested) {// 下游Sink不包含短路操作
            list.forEach(downstream::accept);// 将处理结果传递给流水线下游的Sink
        }
        else {// 4. 下游Sink包含短路操作
            for (T t : list) {// 每次都调用cancellationRequested()询问是否可以结束处理。
                if (downstream.cancellationRequested()) break;
                downstream.accept(t);// 将处理结果传递给流水线下游的Sink
            }
        }
        downstream.end();
        list = null;
    }
    @Override
    public void accept(T t) {
        list.add(t);// 2. 使用当前Sink包装动作处理t,只是简单的将元素添加到中间列表当中
    }
}

当 stream 遇到终态操作时,会触发流水线上的所有 sink 从前到后执行。Stream 类库的设计者通过 Sink AbstractPipeline.opWrapSink(int flags, Sink downstream) 返回一个新的包含了当前 stage 代表的操作以及能够将结果传递给downstream 的 Sink 对象。这样就可以从流水线的最后一个 stage 开始,不断调用上一个 stage 的 opWrapSink() 方法直到最开始(不包括 stage0,因为 stage0 代表数据源,不包含操作),就可以得到一个代表了流水线上所有操作的 Sink,用代码表示就是这样:

// java.util.stream.AbstractPipeline#wrapSink
// 如果最初传入的 Sink 代表结束操作,函数返回时就可以得到一个代表了流水线上所有操作的 Sink
@Override
@SuppressWarnings("unchecked")
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
    Objects.requireNonNull(sink);
    // 从下游向上游不断包装 Sink
    for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
        sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
    }
    return (Sink<P_IN>) sink;
}

opWrapSink 实现示例:

// java.util.stream.IntPipeline#map

@Override
public final IntStream map(IntUnaryOperator mapper) {
    Objects.requireNonNull(mapper);
    return new StatelessOp<Integer>(this, StreamShape.INT_VALUE,
                                    StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
        @Override
        Sink<Integer> opWrapSink(int flags, Sink<Integer> sink) {
            return new Sink.ChainedInt<Integer>(sink) {
                @Override
                public void accept(int t) {
                	// 只是简单封装了下:调用流水线的下一个 stage 执行操作
                    downstream.accept(mapper.applyAsInt(t));
                }
            };
        }
    };
}

现在流水线上从开始到结束的所有的操作都被包装到了一个Sink里,执行这个Sink就相当于执行整个流水线,执行Sink的代码如下:

// java.util.stream.AbstractPipeline#copyInto
// 对 spliterator 代表的数据执行 wrappedSink 代表的操作

@Override
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);
    }
}