Java 8函数式编程(三)-Stream

文章目录

  • Stream的优势
  • Stream的原理和特性
    • 怎么得到Stream
      • Stream接口的静态工厂方法
      • Collection接口的默认方法
  • stream方法使用
    • forEach()
    • filter()
    • map()
    • flatMap()
    • reduce()
    • collect()
  • Stream Pipelines
  • Stream流水线解决方案
    • 操作如何记录?
    • 操作如何叠加?
    • 叠加之后的操作如何执行?
    • 执行后的结果在哪里?
  • 参考文章

Stream的优势

代码简洁:函数式编程写出的代码简洁且意图明确,使用stream接口让你从此告别for循环。
多核友好:Java函数式编程使得编写并行程序从未如此简单,你需要的全部就是调用一下parallel()方法。
常见的stream接口继承关系如图:
Java 8函数式编程(三)-Stream_第1张图片
图中4种stream接口继承自BaseStream,其中IntStream, LongStream, DoubleStream对应三种基本类型(int, long, double,注意不是包装类型),Stream对应所有剩余类型的stream视图。为不同数据类型设置不同stream接口,可以

  1. 提高性能,
  2. 增加特定接口函数。

你可能会奇怪为什么不把IntStream等设计成Stream的子接口?毕竟这接口中的方法名大部分是一样的。答案是这些方法的名字虽然相同,但是返回类型不同,如果设计成父子接口关系,这些方法将不能共存,因为Java不允许只有返回类型不同的方法重载。

Stream的原理和特性

无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

怎么得到Stream

最常用的创建Stream有两种途径:

Stream接口的静态工厂方法

1.of方法:有两个重载方法,一个接受变长参数,一个接口单一值

Stream integerStream = Stream.of(1, 2, 3, 5);
Stream stringStream = Stream.of("taobao");
generator方法:生成一个无限长度的Stream,其元素的生成是通过给定的Supplier
Stream.generate(new Supplier() {
    @Override
    public Double get() {
        return Math.random();
    }
});
Stream.generate(() -> Math.random());
Stream.generate(Math::random);

三条语句的作用都是一样的,只是使用了lambda表达式和方法引用的语法来简化代码。每条语句其实都是生成一个无限长度的Stream,其中值是随机的。这个无限长度Stream是懒加载,一般这种无限长度的Stream都会配合Streamlimit()方法来用。
2. iterate方法:也是生成无限长度的Stream,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seedf(seed),f(f(seed))无限循环

Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);

这段代码就是先获取一个无限长度的正整数集合的Stream,然后取出前10个打印。千万记住使用limit方法,不然会无限打印下去。

Collection接口的默认方法

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

  • 调用Collection.stream()或者Collection.parallelStream()方法
  • 调用Arrays.stream(T[] array)方法

stream方法使用

stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations),二者特点是:

  • 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。中间操作又可以分为无状态的(Stateless)和有状态的(Stateful),无状态中间操作是指元素的处理不受前面元素的影响,而有状态的中间操作必须等到所有元素处理之后才知道最终结果,比如排序是有状态操作,在读取所有元素之前并不能确定排序结果;
  • 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。结束操作又可以分为短路操作和非短路操作,短路操作是指不用处理全部元素就可以返回结果,比如找到第一个满足条件的元素。之所以要进行如此精细的划分,是因为底层对每一种情况的处理方式不同。
    下图汇总了Stream接口的部分常见方法:
    Java 8函数式编程(三)-Stream_第2张图片
    区分中间操作和结束操作最简单的方法,就是看方法的返回值,返回值为stream的大都是中间操作,否则是结束操作。

forEach()

函数原型为void forEach(Consumer action),作用是对容器中的每个元素执行action指定的动作,也就是对元素进行遍历。

Stream<String> stream = Stream.of("stream", "foreach", "method", "test");
stream.forEach(System.out::println);

filter()

函数原型为Stream filter(Predicate predicate),作用是返回一个只包含满足predicate条件元素的Stream
Java 8函数式编程(三)-Stream_第3张图片

List<String> beginningWithNumbers
                = Stream.of("a", "1abc", "abc1")
                .filter(value -> isDigit(value.charAt(0)))
                .collect(toList());
assertEquals(Arrays.asList("1abc"), beginningWithNumbers);

若要重构遗留代码,for循环中的if条件语句就是一个很强的信号,可用filter方法替代。

map()

函数原型为 Stream map(Function mapper),作用是返回一个对当前所有元素执行执行mapper之后的结果组成的Stream。直观的说,就是对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。
Java 8函数式编程(三)-Stream_第4张图片

List<String> collected = Stream.of("a", "b", "hello")
                .map(String::toUpperCase)
                .collect(toList());
assertEquals(Arrays.asList("A", "B", "HELLO"), collected);

若要重构遗留代码,for循环有一个函数需要将一种类型的值转换成另外一种类型,map操作就可以使用该函数。

flatMap()

函数原型为 Stream flatMap(Function> mapper),作用是对每个元素执行mapper指定的操作,并用所有mapper返回的Stream中的元素组成一个新的Stream作为最终返回结果。说起来太拗口,通俗的讲flatMap()的作用就相当于把原stream中的所有元素都"摊平"之后组成的Stream,转换前后元素的个数和类型都可能会改变。
Java 8函数式编程(三)-Stream_第5张图片

List<Integer> together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
                .flatMap(Collection::stream)
                .collect(toList());
assertEquals(Arrays.asList(1, 2, 3, 4), together);

reduce()

规约操作(reduction operation)又被称作折叠操作(fold),是通过某个连接动作将所有元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合,都属于规约操作。Stream类库有两个通用的规约操作reduce()collect(),也有一些为简化书写而设计的专用规约操作,比如sum()max()min()count()等。
reduce操作可以实现从一组元素中生成一个值,sum()max()min()count()等都是reduce操作,将他们单独设为函数只是因为常用。reduce()的方法定义有三种重载形式:

Optional<T> reduce(BinaryOperator<T> accumulator)

T reduce(T identity, BinaryOperator<T> accumulator)

<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

虽然函数定义越来越长,但语义不曾改变,多的参数只是为了指明初始值(参数identity),或者是指定并行执行时多个部分结果的合并方式(参数combiner)。reduce()最常用的场景就是从一堆值中生成一个值。
示例1:从一组单词中找出最长的单词。这里“大”的含义就是“长”。

// 找出最长的单词
Stream<String> stream = Stream.of("reduce", "optional", "test", "stream");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length() >= s2.length() ? s1 : s2);
//Optional longest = stream.max(Comparator.comparingInt(String::length));
assertEquals("optional", longest.get());

示例2:数字求和
Java 8函数式编程(三)-Stream_第6张图片

int sum = Stream.of(1, 2, 3, 4)
                .reduce(0, (acc, element) -> acc + element);
assertEquals(10, sum);

示例3:求字符串数组长度之和

// 求单词长度之和
Stream<String> stream = Stream.of("a", "bb", "ccc", "dddd");
int lengthSum = stream.reduce(0,            // 初始值 // (1)
    (sum, str) -> sum+str.length(),         // 累加器 // (2)
    (a, b) -> a+b);                         // 部分和拼接器,并行执行时才会用到 // (3)
// int lengthSum = stream.mapToInt(String::length).sum();
assertEquals(10, lengthSum);

collect()

如果你发现某个功能在Stream接口中没找到,十有八九可以通过collect()方法实现。collect()Stream接口方法中最灵活的一个,学会它才算真正入门Java函数式编程。

List<SaleOrderLineDO> newSaleOrderLines = saleOrderLines.stream()
            .map(oldSaleOrderLine -> {
                SaleOrderLineDO saleOrderLine = new SaleOrderLineDO();
                saleOrderLine.setId(oldSaleOrderLine.getId());
                saleOrderLine.setBuyerId(oldSaleOrderLine.getBuyerId());
                saleOrderLine.setReceiveQuantity(oldSaleOrderLine.getQuantity());
                saleOrderLine.setGmtModified(new Date());
                saleOrderLine.setModifier(modifier);
                return saleOrderLine;
            }).collect(Collectors.toList());

Map<Long, Integer> purchaseSendQuantityMap = realQuantities.stream()
            .collect(Collectors.toMap(RealQuantity::getSubLineId, RealQuantity::getSendQuantity));

Stream Pipelines

前面我们也体验了Stream的简洁、简单、简约的代码风格。那么到底Stream的底层是如何实现的呢?下面我们用一个简单的例子来一步一步的理解下。一个栗子:从一个字符串列表中找到以‘a’开头的、最长的字符串长度。
一种简单的方式是每一次函数调用都执行一次迭代,这样并将处理中间结果放到某种数据结构中(比如数组,容器等)。

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(), 保留最长的长度
}
assertEquals(4, maxLength);

具体说来,就是调用filter()方法后立即执行,选出所有以A开头的字符串并放到一个列表list1中,之后让list1传递给mapToInt()方法并立即执行,生成的结果放到list2中,最后遍历list2找出最大的数字作为最终结果。程序的执行流程如如所示:
虽然这样做实现起来非常简单直观,但有两个明显的弊端:

  1. 迭代次数多。迭代次数跟函数调用的次数相等。
  2. 频繁产生中间结果。每次函数调用都产生一次中间结果,存储开销无法接受。
    Java 8函数式编程(三)-Stream_第7张图片
    这些弊端使得效率底下,根本无法接受。如果不使用以上以上方式,我们都知道上述需求该如何在一次迭代中完成,大致是如下形式:
int maxLength = 0;
        List<String> strings = Arrays.asList("a", "bb", "ccc", "abcd");
        for(String str : strings){
            if(str.startsWith("a")){                    // 1. filter(), 保留以A开头的字符串
                int len = str.length();                 // 2. mapToInt(), 转换成长度
                maxLength = Math.max(len, maxLength);   // 3. max(), 保留最长的长度
            }
        }
assertEquals(4, maxLength);

采用这种方式我们不但减少了迭代次数,也避免了存储中间结果,显然这就是流水线,因为我们把三个操作放在了一次迭代当中。只要我们事先知道用户意图,总是能够采用上述方式实现跟Stream API等价的功能,但问题是Stream类库的设计者并不知道用户的意图是什么。如何在无法假设用户行为的前提下实现流水线,是类库的设计者要考虑的问题。
采用Stream的方式的代码如下。

int maxLength = Stream.of("a", "bb", "ccc", "abcd")
                .filter(value -> value.startsWith("a"))
                .mapToInt(String::length)
                .max()
                .orElse(0);
assertEquals(4, maxLength);

Java 8函数式编程(三)-Stream_第8张图片

Stream流水线解决方案

我们大致能够想到,应该采用某种方式记录用户每一步的操作,当用户调用结束操作时将之前记录的操作叠加到一起在一次迭代中全部执行掉。沿着这个思路,有几个问题需要解决:

  1. 用户的操作如何记录?
  2. 操作如何叠加?
  3. 叠加之后的操作如何执行?
  4. 执行后的结果(如果有)在哪里?
    以下代码为例:
Stream.of("onE", "twO", "threE", "fouR")
//    .parallel()
        .filter(e -> e.length() > 3)
        .map(String::toLowerCase)
        .sorted()
        .map(String::toUpperCase)
        .forEach(System.out::println);

操作如何记录?

注意这里使用的是“操作(operation)”一词,指的是“Stream中间操作”的操作,很多Stream操作会需要一个回调函数(Lambda表达式),因此一个完整的操作是<数据来源,操作,回调函数>构成的三元组。Stream中使用Stage的概念来描述一个完整的操作,并用某种实例化后的PipelineHelper来代表Stage,将具有先后顺序的各个Stage连到一起,就构成了整个流水线。跟Stream相关类和接口的继承关系图示。
Java 8函数式编程(三)-Stream_第9张图片
由于篇幅原因,DoublePipeline没在图中展示,DoublePipelineIntPipelineLongPipeline这三个类专门为三种基本类型而定制的,跟ReferencePipeline是并列关系。图中Head用于表示第一个Stage,即调用调用诸如Collection.stream()方法产生的Stage,很显然这个Stage里不包含任何操作;StatelessOpStatefulOp分别表示无状态和有状态的Stage,对应于无状态和有状态的中间操作。
Stream流水线组织结构示意图如下:
Java 8函数式编程(三)-Stream_第10张图片
图中通过Stream.of()方法得到Head Stage,紧接着调用一系列的中间操作,不断产生新的Stream。这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。这就是Stream记录操作的方式。
对于AbstractPipeline,有两个构造函数,分别生成headintermediatepipeline stage

/**
 * 构造函数:生成Pipeline的头
 *
 * @param source {@code Spliterator} describing the stream source
 * @param sourceFlags the source flags for the stream source, described in
 * {@link StreamOpFlag}
 * @param parallel {@code true} if the pipeline is parallel
 */
AbstractPipeline(Spliterator<?> source,
                 int sourceFlags, boolean parallel) {
    this.previousStage = null;
    this.sourceSpliterator = source;
    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;
    this.depth = 0;
    this.parallel = parallel;
}

/**
 * 构造函数:将中间stage连接到当前pipeline尾部
 *
 * @param previousStage the upstream pipeline stage
 * @param opFlags the operation flags for the new stage, described in
 * {@link StreamOpFlag}
 */
AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
    if (previousStage.linkedOrConsumed)
        throw new IllegalStateException(MSG_STREAM_LINKED);
    previousStage.linkedOrConsumed = true;
    previousStage.nextStage = this;

    this.previousStage = previousStage;
    this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK;
    this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags);
    this.sourceStage = previousStage.sourceStage;
    if (opIsStateful())
        sourceStage.sourceAnyStateful = true;
    this.depth = previousStage.depth + 1;
}

操作如何叠加?

以上只是解决了操作记录的问题,要想让流水线起到应有的作用我们需要一种将所有操作叠加到一起的方案。你可能会觉得这很简单,只需要从流水线的head开始依次执行每一步的操作(包括回调函数)就行了。这听起来似乎是可行的,但是你忽略了前面的Stage并不知道后面Stage到底执行了哪种操作,以及回调函数是哪种形式。换句话说,只有当前Stage本身才知道该如何执行自己包含的动作。这就需要有某种协议来协调相邻Stage之间的调用关系。
这种协议由Sink接口完成,Sink接口包含的方法如下表所示:

方法名 作用
void begin(long size) 开始遍历元素之前调用该方法,通知Sink做好准备。
void end() 所有元素遍历完成之后调用,通知Sink没有更多的元素了。
boolean cancellationRequested() 是否可以结束操作,可以让短路操作尽早结束。
void accept(T t) 遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage只需要调用当前Stage.accept(T t)方法就行了。

有了上面的协议,相邻Stage之间调用就很方便了,每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stageaccept()方法即可,并不需要知道其内部是如何处理的。当然对于有状态的操作,Sinkbegin()end()方法也是必须实现的。比如Stream.sorted()是一个有状态的中间操作,其对应的Sink.begin()方法可能创建一个乘放结果的容器,而accept()方法负责将元素添加到该容器,最后end()负责对容器进行排序。对于短路操作,Sink.cancellationRequested()也是必须实现的,比如Stream.findFirst()是短路操作,只要找到一个元素,cancellationRequested()就应该返回true,以便调用者尽快结束查找。Sink的四个接口方法常常相互协作,共同完成计算任务。实际上Stream API内部实现的的本质,就是如何重载Sink的这四个接口方法。
有了Sink对操作的包装,Stage之间的调用问题就解决了,执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了。一种可能的Sink.accept()方法流程是这样的:

void accept(U u){
    1. 使用当前Sink包装的回调函数处理u
    2. 将处理结果传递给流水线下游的Sink
}

Sink接口的其他几个方法也是按照这种[处理->转发]的模型实现。下面我们结合具体例子看看Stream的中间操作是如何将自身的操作包装成Sink以及Sink是如何将处理结果转发给下一个Sink的。先看Stream.map()方法:

// java.util.stream.ReferencePipeline#map
// 调用该方法将产生一个新的Stream
public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
   Objects.requireNonNull(mapper);
   return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
       @Override /*opWrapSink()方法返回由回调函数包装而成Sink,由wrapSink()触发*/
       Sink<P_OUT> opWrapSink(int flags, Sink<R> downstream) {
           return new Sink.ChainedReference<P_OUT, R>(downstream) {
               @Override
               public void accept(P_OUT u) {
                   R r = mapper.apply(u);    // 1. 使用当前Sink包装的回调函数mapper处理u
                   downstream.accept(r);    // 2. 将处理结果传递给流水线下游的Sink
               }
           };
       }
   };
}

上述代码看似复杂,其实逻辑很简单,就是将回调函数mapper包装到一个Sink当中。由于Stream.map()是一个无状态的中间操作,所以map()方法返回了一个StatelessOp内部类对象(一个新的Stream),调用这个新StreamopWrapSink()方法将得到一个包装了当前回调函数的Sink

// java.util.stream.Sink.ChainedReference: 244行
static abstract class ChainedReference<T, E_OUT> implements Sink<T> {
    protected final Sink<? super E_OUT> downstream;

    public ChainedReference(Sink<? super E_OUT> downstream) {
        this.downstream = Objects.requireNonNull(downstream);
    }

    @Override
    public void begin(long size) {
        downstream.begin(size);
    }

    @Override
    public void end() {
        downstream.end();
    }

    @Override
    public boolean cancellationRequested() {
        return downstream.cancellationRequested();
    }
}

再来看一个复杂一点的例子。Stream.sorted()方法将对Stream中的元素进行排序,显然这是一个有状态的中间操作,因为读取所有元素之前是没法得到最终顺序的。抛开模板代码直接进入问题本质,sorted()方法是如何将操作封装成Sink的呢?sorted()一种可能封装的Sink代码如下:

// 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,只是简单的将元素添加到中间列表当中
    }
}

上述代码完美的展现了Sink的四个接口方法是如何协同工作的:
首先beging()方法告诉Sink参与排序的元素个数,方便确定中间结果容器的的大小;
之后通过accept()方法将元素添加到中间结果当中,最终执行时调用者会不断调用该方法,直到遍历所有元素;
最后end()方法告诉Sink所有元素遍历完毕,启动排序步骤,排序完成后将结果传递给下游的Sink
如果下游的Sink是短路操作,将结果传递给下游时不断询问下游cancellationRequested()是否可以结束处理。
下图为Sink的构建图
Java 8函数式编程(三)-Stream_第11张图片

叠加之后的操作如何执行?

Sink完美封装了Stream每一步操作,并给出了[处理->转发]的模式来叠加操作。这一连串的齿轮已经咬合,就差最后一步拨动齿轮启动执行。是什么启动这一连串的操作呢?也许你已经想到了启动的原始动力就是结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。
Java 8函数式编程(三)-Stream_第12张图片
结束操作之后不能再有别的操作,所以结束操作不会创建新的流水线阶段(Stage),直观的说就是流水线的链表不会在往后延伸了。结束操作会创建一个包装了自己操作的Sink,这也是流水线中最后一个Sink,这个Sink只需要处理数据而不需要将结果传递给下游的Sink(因为没有下游)。对于Sink的[处理->转发]模型,结束操作的Sink就是调用链的出口。
我们再来考察一下上游的Sink是如何找到下游Sink的。一种可选的方案是在PipelineHelper中设置一个Sink字段,在流水线中找到下游Stage并访问Sink字段即可。但Stream类库的设计者没有这么做,而是设置了一个Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法来得到Sink,该方法的作用是返回一个新的包含了当前Stage代表的操作以及能够将结果传递给downstreamSink对象。为什么要产生一个新对象而不是返回一个Sink字段?这是因为使用opWrapSink()可以将当前操作与下游Sink(上文中的downstream参数)结合成新Sink。试想只要从流水线的最后一个Stage开始,不断调用上一个StageopWrapSink()方法直到最开始(不包括stage0,因为stage0代表数据源,不包含操作),就可以得到一个代表了流水线上所有操作的Sink,用代码表示就是这样:

// java.util.stream.AbstractPipeline#wrapSink:513行
// 从下游向上游不断包装Sink。
// 如果最初传入的Sink代表结束操作,函数返回时就可以得到一个代表了流水线上所有操作的Sink。
@Override
@SuppressWarnings("unchecked")
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
    Objects.requireNonNull(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;
}

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

// java.util.stream.AbstractPipeline#copyInto:476行
// 对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);
    }
}

上述代码首先调用wrappedSink.begin()方法告诉Sink数据即将到来,然后调用spliterator.forEachRemaining()方法对数据进行迭代(Spliterator是容器的一种迭代器),最后调用wrappedSink.end()方法通知Sink数据处理结束。
Java 8函数式编程(三)-Stream_第13张图片

执行后的结果在哪里?

最后一个问题是流水线上所有操作都执行后,用户所需要的结果(如果有)在哪里?首先要说明的是不是所有的Stream结束操作都需要返回结果,有些操作只是为了使用其副作用(Side-effects),比如使用Stream.forEach()方法将结果打印出来就是常见的使用副作用的场景(事实上,除了打印之外其他场景都应避免使用副作用),对于真正需要返回结果的结束操作结果存在哪里呢?
特别说明:副作用不应该被滥用,也许你会觉得在Stream.forEach()里进行元素收集是个不错的选择,就像下面代码中那样,但遗憾的是这样使用的正确性和效率都无法保证,因为Stream可能会并行执行。大多数使用副作用的地方都可以使用归约操作更安全和有效的完成。

// 错误的收集方式
ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
      .forEach(s -> results.add(s));  // Unnecessary use of side-effects!
// 正确的收集方式
List<String>results =
     stream.filter(s -> pattern.matcher(s).matches())
             .collect(Collectors.toList());  // No side-effects!

回到流水线执行结果的问题上来,需要返回结果的流水线结果存在哪里呢?这要分不同的情况讨论,下表给出了各种有返回结果的Stream结束操作。

返回类型 对应的结束操作
boolean anyMatch()
allMatch()
noneMatch()
Optional findFirst()
findAny()
归约结果 reduce()
collect()
数组 toArray()

对于表中返回boolean或者Optional的操作(Optional是存放一个值的容器)的操作,由于值返回一个值,只需要在对应的Sink中记录这个值,等到执行结束时返回就可以了。
对于归约操作,最终结果放在用户调用时指定的容器中(容器类型通过收集器指定)。collect(), reduce(), max(), min()都是归约操作,虽然max()min()也是返回一个Optional,但事实上底层是通过调用reduce()方法实现的。
对于返回是数组的情况,毫无疑问的结果会放在数组当中。这么说当然是对的,但在最终返回数组之前,结果其实是存储在一种叫做Node的数据结构中的。Node是一种多叉树结构,元素存储在树的叶子当中,并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便。
对于collect这样的操作是需要拿到最终end产生的结果.end产生的结果在最后一个Sink中,这样的操作最终都会提供一个取出数据的get方法.

// java.util.stream.AbstractPipeline#evaluate(java.util.stream.TerminalOp) 226行
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()));
}
// java.util.stream.ReduceOps.ReduceOp: 684行 
private static abstract class ReduceOp<T, R, S extends AccumulatingSink<T, R, S>>
        implements TerminalOp<T, R> {
    @Override
    public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
                                       Spliterator<P_IN> spliterator) {
        return helper.wrapAndCopyInto(makeSink(), spliterator).get();
    }

    @Override
    public <P_IN> R evaluateParallel(PipelineHelper<T> helper,
                                     Spliterator<P_IN> spliterator) {
        return new ReduceTask<>(this, helper, spliterator).invoke().get();
    }
}

参考文章

  1. Streams API(I)
  2. Streams API(II)
  3. 深入理解Java Stream流水线
  4. Java8 Stream原理深度解析

你可能感兴趣的:(java)