代码简洁:函数式编程写出的代码简洁且意图明确,使用stream接口让你从此告别for循环。
多核友好:Java函数式编程使得编写并行程序从未如此简单,你需要的全部就是调用一下parallel()
方法。
常见的stream接口继承关系如图:
图中4种stream接口继承自BaseStream
,其中IntStream
, LongStream
, DoubleStream
对应三种基本类型(int
, long
, double
,注意不是包装类型),Stream
对应所有剩余类型的stream视图。为不同数据类型设置不同stream接口,可以
你可能会奇怪为什么不把IntStream
等设计成Stream
的子接口?毕竟这接口中的方法名大部分是一样的。答案是这些方法的名字虽然相同,但是返回类型不同,如果设计成父子接口关系,这些方法将不能共存,因为Java不允许只有返回类型不同的方法重载。
无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
为函数式编程而生。对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
都会配合Stream
的limit()
方法来用。
2. iterate
方法:也是生成无限长度的Stream
,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seed
,f(seed)
,f(f(seed))
无限循环
Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);
这段代码就是先获取一个无限长度的正整数集合的Stream,然后取出前10个打印。千万记住使用limit方法,不然会无限打印下去。
Collection
接口有一个stream
方法,所以其所有子类都都可以获取对应的Stream
对象。
Collection.stream()
或者Collection.parallelStream()
方法Arrays.stream(T[] array)
方法对stream
的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations),二者特点是:
函数原型为void forEach(Consumer super E> action),作用是对容器中的每个元素执行action指定的动作,也就是对元素进行遍历。
Stream<String> stream = Stream.of("stream", "foreach", "method", "test");
stream.forEach(System.out::println);
函数原型为Stream
,作用是返回一个只包含满足predicate
条件元素的Stream
。
List<String> beginningWithNumbers
= Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
assertEquals(Arrays.asList("1abc"), beginningWithNumbers);
若要重构遗留代码,for循环中的if条件语句就是一个很强的信号,可用filter
方法替代。
函数原型为
,作用是返回一个对当前所有元素执行执行mapper
之后的结果组成的Stream
。直观的说,就是对每个元素按照某种操作进行转换,转换前后Stream
中元素的个数不会改变,但元素的类型取决于转换之后的类型。
List<String> collected = Stream.of("a", "b", "hello")
.map(String::toUpperCase)
.collect(toList());
assertEquals(Arrays.asList("A", "B", "HELLO"), collected);
若要重构遗留代码,for循环有一个函数需要将一种类型的值转换成另外一种类型,map操作就可以使用该函数。
函数原型为
,作用是对每个元素执行mapper
指定的操作,并用所有mapper
返回的Stream
中的元素组成一个新的Stream
作为最终返回结果。说起来太拗口,通俗的讲flatMap()
的作用就相当于把原stream
中的所有元素都"摊平"之后组成的Stream
,转换前后元素的个数和类型都可能会改变。
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);
规约操作(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());
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);
如果你发现某个功能在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
的简洁、简单、简约的代码风格。那么到底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找出最大的数字作为最终结果。程序的执行流程如如所示:
虽然这样做实现起来非常简单直观,但有两个明显的弊端:
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);
我们大致能够想到,应该采用某种方式记录用户每一步的操作,当用户调用结束操作时将之前记录的操作叠加到一起在一次迭代中全部执行掉。沿着这个思路,有几个问题需要解决:
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
相关类和接口的继承关系图示。
由于篇幅原因,DoublePipeline
没在图中展示,DoublePipeline
、IntPipeline
、LongPipeline
这三个类专门为三种基本类型而定制的,跟ReferencePipeline
是并列关系。图中Head用于表示第一个Stage
,即调用调用诸如Collection.stream()
方法产生的Stage
,很显然这个Stage
里不包含任何操作;StatelessOp
和StatefulOp
分别表示无状态和有状态的Stage
,对应于无状态和有状态的中间操作。
Stream
流水线组织结构示意图如下:
图中通过Stream.of()
方法得到Head Stage
,紧接着调用一系列的中间操作,不断产生新的Stream
。这些Stream
对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage
都记录了前一个Stage
和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。这就是Stream
记录操作的方式。
对于AbstractPipeline
,有两个构造函数,分别生成head
、intermediate
的pipeline 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
只需调用后一个Stage
的accept()
方法即可,并不需要知道其内部是如何处理的。当然对于有状态的操作,Sink
的begin()
和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
),调用这个新Stream
的opWrapSink()
方法将得到一个包装了当前回调函数的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
的构建图
Sink
完美封装了Stream
每一步操作,并给出了[处理->转发]的模式来叠加操作。这一连串的齿轮已经咬合,就差最后一步拨动齿轮启动执行。是什么启动这一连串的操作呢?也许你已经想到了启动的原始动力就是结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。
结束操作之后不能再有别的操作,所以结束操作不会创建新的流水线阶段(Stage
),直观的说就是流水线的链表不会在往后延伸了。结束操作会创建一个包装了自己操作的Sink
,这也是流水线中最后一个Sink
,这个Sink
只需要处理数据而不需要将结果传递给下游的Sink
(因为没有下游)。对于Sink
的[处理->转发]模型,结束操作的Sink
就是调用链的出口。
我们再来考察一下上游的Sink
是如何找到下游Sink
的。一种可选的方案是在PipelineHelper
中设置一个Sink
字段,在流水线中找到下游Stage
并访问Sink
字段即可。但Stream
类库的设计者没有这么做,而是设置了一个Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)
方法来得到Sink
,该方法的作用是返回一个新的包含了当前Stage代表的操作以及能够将结果传递给downstream
的Sink
对象。为什么要产生一个新对象而不是返回一个Sink
字段?这是因为使用opWrapSink()
可以将当前操作与下游Sink
(上文中的downstream
参数)结合成新Sink
。试想只要从流水线的最后一个Stage
开始,不断调用上一个Stage
的opWrapSink()
方法直到最开始(不包括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
数据处理结束。
最后一个问题是流水线上所有操作都执行后,用户所需要的结果(如果有)在哪里?首先要说明的是不是所有的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();
}
}