Stream是Java8的新特性之一,从名字看出来,它的含义是流,所谓的流就是一个数据渠道,可以用于操作集合所生成的元素序列。 实际上不光是集合,包括数组、文件等,只要是可以转换成流,我们都可以借助流式处理,类似于我们写SQL语句一样对其进行操作。
1)Stream自己不会存储元素。
2)Stream不会改变源对象。
3)Stream操作是延时执行的,这意味着它们会等到需要结果时才执行。
Stream的流式处理可以分为三个部分:转换成流、中间操作、终端操作,如下图:
具体语句如下:
List userIds = userLists.stream().map(UserDO::getId).collect(Collectors.toList());
stream语句将userLists集合转换成了一个流,map为中间操作,通过函数式表达式获取user的Id,最后通过终端操作将其转换成一个list集合返回。
可以发现,Stream极大地简化了我们对集合的操作。
1) 通过Collection得Stream()方法(串行流)或者 parallelStream()方法(并行流)创建Stream。
@Test
public void test1 () {
//1. 通过Collection得Stream()方法(串行流)
//或者 parallelStream()方法(并行流)创建Stream
List<String> list = new ArrayList<String>();
Stream<String> stream1 = list.stream();
Stream<String> stream2 = list.parallelStream();
}
2) 通过Arrays中得静态方法stream()获取数组流 。
@Test
public void test2 () {
//2. 通过Arrays中得静态方法stream()获取数组流
IntStream stream = Arrays.stream(new int[]{
3,5});
}
3) 通过Stream类中得 of()静态方法获取流 。
@Test
public void test3 () {
//3. 通过Stream类中得 of()静态方法获取流
Stream<String> stream = Stream.of("4645", "huinnj");
}
4) 创建无限流(迭代、生成) 。
@Test
public void test4 () {
//4. 创建无限流
//迭代(需要传入一个种子,也就是起始值,然后传入一个一元操作)
Stream<Integer> stream1 = Stream.iterate(2, (x) -> x * 2);
//生成(无限产生对象)
Stream<Double> stream2 = Stream.generate(() -> Math.random());
}
distinct: 对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素。
filter方法是根据设置的条件来过滤、筛选出所需要的元素。
map方法是对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。
limit方法是根据设定的数量从流中获取对应数量的元素。
skip方法与limt有点区别,此方法是获取到根据设定的数量n去除掉集合中前n个元素之后的所有数据。
sorted方法是对流中的元素进行排序,降序和升序都可以使用自然排序的方法,也可以调用Comparator中的方法进行排序
peek 方法是生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数
注:刚开始以为peek与map的用法一样,后面通过学习及练习,peek方法其实是没有返回值的,多半用于一些输出,数据的校验等。
接收一个函数作为参数,将流中的每个值都换成一个流,然后把所有流连接成一个流。
检查是否匹配所有元素。
检查是否至少匹配所有元素 。
检查是否没有匹配所有元素 。
返回第一个元素。
@Test
public void test16 () {
Optional<Person> person = persons.stream().findFirst();
System.out.println(person);
person.orElse(new Person("王五", "男", 35, Status.BUSY));
}
返回当前流中任意元素。
返回流中元素总个数。
返回流中最大值。
返回流中最小值。
有reduce(T identitty,BinaryOperator) 与reduce(BinaryOperator),前者需要一个起始值,然后传入二元运算,后者没有起始值。
Collector接口中方法得实现决定了如何对流执行收集操作(如收集到List,Set,Map)。但是Collectors实用类提供了很多静态方法,可以方便地创建常见得收集器实例。
先来看下列语句:
List<String> list = Arrays.asList("Hello","World","Word");
// 求出以W开头,长度最长的单词,并把长度赋值给num
Integer num = list.stream().filter(x -> x.startsWith("W")).mapToInt(String::length).max().getAsInt();
逻辑很简单,使用一行stream()语句即可完成需求,但是这里有一个问题,stream()究竟是怎么做到用一行来完成这么多筛选条件的语句呢?它是每调用一次中间操作就遍历一遍集合元素吗?答案是不是的,我们来看看stream()具体是怎么操作的。
首先我们来看看一般的for循环是怎么在一次迭代的情况下完成的:
int maxLength = 0;
for (String str : list) {
if (str.startsWith("W")) {
maxLength = Math.max(str.length(), maxLength);
}
}
这种方式不但只用了一次迭代,同时避免了存储中间结果,但是它有个前提,必须得知道当前的程序意图。而Stream设计者并不能知道用户想要实现的功能是什么。因此,在无法假设用户行为的前提下实现流水线,是设计者首要考虑的问题。
在这里,大家应该都能想到,如果能够像队列一样记录用户的每一次操作,在用户结束操作的时候,再将所有的记录叠加到一次迭代中全部执行掉,那不就解决问题了吗。那么接下来又会有一系列的问题:
1)用户的操作该怎么记录?
2)操作如何叠加?
3)叠加之后的操作如何?
4)执行后的结果在哪里?
注意,这里的操作是指中间操作,Stream中会使用Stage概念来描述一个完整的操作,并用某种实例化后的PipelineHelper来代表Stage,将具有先后顺序的各个Stage连到一起,就构成了整个流水线,其中Stream相关类和接口的继承关系如上图所示。
Stream流水线组织结构示意图如下:
通过Collection.stream()方法得到Head,也就是stage0,紧接着调用一系列中间操作,不断产生新的Stream,这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。
Head记录Stream起始操作。
StatelessOp记录无状态的中间操作。
StatefulOp记录有状态的中间操作 。
Reference构造方法
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;
}
Reference构造方法调用了AbstractPipeline的构造方法,可以发现,它的构造函数更像是链表的操作,指定了前驱stage的后继stage为当前stage。
Head构造方法
AbstractPipeline(Supplier<? extends Spliterator<?>> source,
int sourceFlags, boolean parallel) {
this.previousStage = null;
this.sourceSupplier = 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;
}
可以发现,Head的构造方法指定了sourceStage为当前stage。
现在我们知道Stream()是如何记录每一步的操作了,要想让流水线将所有的操作叠加在一起,还需要有一种协议来协调相邻的Stage之间的关系。这个协议就是Sink接口,Sink接口方法如下:
有了此协议,相邻Stage之间调用就很方便了,每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stage的accept()方法即可,并不需要知道内部是如何处理的。对于短路操作,也要实现cancellationRequested()。实际上Stream API内部实现的本质,就是如何重载Sink的这四个接口方法。
有了Sink包装,流水线调用时只需要从流水线的head开始,对数据源依次调用每个Stage对应的Sink{begin(),accept(),cancellationRequested(),end()}就可以了。就类似于[处理->转发]这种模型。
让我们以**Stream.map()**为例,探索Sink是怎么实现的:
// 产生一个新的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) {
// opWrapSink返回回调函数包装成的Sink
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
return new Sink.ChainedReference<P_OUT, R>(sink) {
@Override
public void accept(P_OUT u) {
// 将处理结果传递给流水线下游
downstream.accept(mapper.apply(u));
}
};
}
};
}
上述逻辑总结如下:首先,将回调函数mapper包装到一个Sink中,由于Stream.map()是一个无状态的中间操作,所以返回一个StatelessOp对象,调用这个新Stream的opWripSink()得到一个包装了当前回调函数的Sink。
接下来我们看一看**Stream.sorted()**方法:
// Stream.sort()方法用到的Sink实现
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) {
...
// 创建一个存放排序元素的列表
list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
}
@Override
public void end() {
list.sort(comparator);// 只有元素全部接收之后才能开始排序
downstream.begin(list.size());
if (!cancellationWasRequested) {
// 下游Sink不包含短路操作
list.forEach(downstream::accept);// 2. 将处理结果传递给流水线下游的Sink
}
else {
// 下游Sink包含短路操作
for (T t : list) {
// 每次都调用cancellationRequested()询问是否可以结束处理。
if (downstream.cancellationRequested()) break;
downstream.accept(t);// 2. 将处理结果传递给流水线下游的Sink
}
}
downstream.end();
list = null;
}
@Override
public void accept(T t) {
list.add(t);// 1. 使用当前Sink包装动作处理t,只是简单的将元素添加到中间列表当中
}
}
1)首先beging()方法告诉Sink参与排序的元素个数,方便确定中间结果容器的的大小;
2)之后通过accept()方法将元素添加到中间结果当中,最终执行时调用者会不断调用该方法,直到遍历所有元素;
3)最后end()方法告诉Sink所有元素遍历完毕,启动排序步骤,排序完成后将结果传递给下游的Sink;
4)如果下游的Sink是短路操作,将结果传递给下游时不断询问下游cancellationRequested()是否可以结束处理。
既然Sink已经将Stream的每一步操作都进行了封装,就差最后一步执行了,那么究竟是谁来执行呢?没错,就是我们一开始提到的结束操作。
结束操作之后不能再有别的操作,所以结束操作会创建一个包装了自己操作的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,用代码表示就是这样:
// AbstractPipeline.wrapSink()
// 从下游向上游不断包装Sink。如果最初传入的sink代表结束操作,
// 函数返回时就可以得到一个代表了流水线上所有操作的Sink。
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
...
for (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的代码如下:
// AbstractPipeline.copyInto(), 对spliterator代表的数据执行wrappedSink代表的操作。
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
...
if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知开始遍历
spliterator.forEachRemaining(wrappedSink);// 迭代
wrappedSink.end();// 通知遍历结束
}
...
}
1)对于表中返回boolean或者Optional的操作(Optional是存放 一个 值的容器)的操作,由于值返回一个值,只需要在对应的Sink中记录这个值,等到执行结束时返回就可以了。
2)对于归约操作,最终结果放在用户调用时指定的容器中(容器类型通过收集器指定)。collect(), reduce(), max(), min()都是归约操作,虽然max()和min()也是返回一个Optional,但事实上底层是通过调用reduce()方法实现的。
3)于返回是数组的情况,毫无疑问的结果会放在数组当中。这么说当然是对的,但在最终返回数组之前,结果其实是存储在一种叫做Node的数据结构中的。Node是一种多叉树结构,元素存储在树的叶子当中,并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便。