摘要
Stream API提供了一系列对集合的强大操作,配合lambda表达式,可让集合处理非常简洁清晰。本文主要介绍了Stream流的用法,包括集合接口(Collection
)与流(Stream
)接口的互相转换,Stream流的父类BaseStream
接口中的方法介绍、串行流/并行流和无序流、缩减操作、映射操作。
集合与流的互相转换
集合转Stream流
使用Collection
接口的stream()
和parrallelStream()
方法
List<Integer> numbers = Arrays.asList(1, 2, 3);
Stream<Integer> numStream = numbers.stream();
// parrallelStream()方法将集合转换为并行流
Stream<Integer> numPStream = numbers.parrallelStream();
关于什么是并行流后面会讲。
流除了可以从集合转换,也可以使用Stream.of()方法直接创建,有针对int, long, double的专用流IntStream,LongStream,DoubleStream
Stream流转为集合
在借助Stream API完成对集合的复杂操作后,可能需要转换回集合,即收集操作。这就需要Stream接口的collect方法, 它有两种实现
// R代表集合类型
// 第一种
R collect(Collector collector);
// 第二种
R collector(Supplier supplier, BiConsumer accumulator, BiConsumer cobiner)
1,第一种
参数是Collector收集器接口,很少需要使用自定义收集器,大部分场景使用JDK提供Collectors工具类,它提供了大量的静态收集器方法如Collectors.toList()
,Collectors.toSet()
// 将前文中的numStream转回集合
List<Integer> newNumbers = numStream.collect(Colletors.toList());
2,第二种
少数情况下需要定制收集器,supplier
指定如何创建用于保存结果的对象,accumulator
函数是将一个元素添加到结果,combiner
函数合并两个部分的结果,这三个参数类型都是函数式接口。
// 函数式接口可以使用lambda表达式简化代码
List<Integer> newNumbers = newStream.collect(()-> new ArrayList(),
(list,e) -> list.add(e),
(listA,listB) -> listA.addAll(listB);
);
// 使用方法引用进一步简化
List<Integer> newNumbers = newStream.collect(List::new, List::add, List::addAll);
如果你还不知道lambda表达式,参考java8新特性-lambda表达式
BaseStream接口
Stream API定义流主要行为的是BaseStream
接口
public interface BaseStream<T, S extends BaseStream<T, S>> extends AutoCloseable {
//获得流的迭代器,并返回对该迭代器的引用(终端操作)
Iterator<T> iterator();
//获取流的并行迭代器spliterator,并返回其引用(终端操作)
Spliterator<T> spliterator();
// 如果调用流是一个并行流,则返回true;如果调用流是一个顺序流,则返回false
boolean isParallel();
// 基于调用流,返回一个顺序流。如果调用流已经是顺序流了,就返回该流。(中间操作)
S sequential();
// 基于调用流,返回一个并行流。如果调用流已经是并行流了,就返回该流。(中间操作)
S parallel();
// 基于调用流,返回一个无序流。如果调用流已经是无序流了,就返回该流。(中间操作)
S unordered();
// 返回一个新流,closeHandler指定了该流的关闭处理程序,当关闭该流时,将调用这个处理程序
S onClose(Runnable closeHandler);
// 父类AutoCloseable定义的方法,调用注册关闭处理程序,关闭调用流(很少会被使用到)
@Override
void close();
}
每个方法的功能现在不必全部明白,后面会一个一个讲。这里只需要注意注释中有些方法属于终端操作(terminal operations),有些方法属于中间操作(intermediate operations)。
中间操作与终端操作
1, 终端操作
- 会消费流,产生一个结果或执行某种操作
- 一个流被消费过就无法再重用
2,中间操作
- 产生另一个流,可以用来创建执行一系列动作的管道
- 中间操作不是立刻发生的,而是在中间操作创建的新流上执行完终端操作后才发生
中间操作分为“有状态”和“无状态” :
1, 无状态操作
对流中的每个元素单独处理,与流中其他元素无关, 如谓词过滤
2,有状态操作
某个元素的处理依赖其他元素,如排序
由BaseStream
接口派生出的流接口包括IntStream
,LongStream
,DoubleStream
,Stream<T>
//这是最具代表性的接口
public interface Stream<T> extends BaseStream<T, Stream<T>> {
//....
}
//非常贴心的提供了一些基本类型的Stream
public interface IntStream extends BaseStream<Integer, IntStream> {
}
public interface LongStream extends BaseStream<Long, LongStream> {
}
public interface DoubleStream extends BaseStream<Double, DoubleStream> {
}
串行流、并行流和无序流
关于流中的顺序
说一个流是有序的是指流中元素的出现顺序(encounter order)是已经定义好的,否则就是无序的。流是否有定义好的元素出现顺序(encounter order)取决于数据源和中间操作(intermediate operations)。
- 某些数据源(如
List
和Arrays
)有内在的顺序,而其他一些数据源(如HashSet
)则没有 - 一些中间操作,如
sorted()
,会给流加上顺序,而一些操作则会将一个有序的流变成无序的,如BaseStream.unordered()
另外,一些终端操作(terminal operationis)会无视元素的出现顺序,如forEach()
。
如果一个流是有序的,大多数的操作都会被限制按照元素的出现顺序执行。例如一个流的数据源是List,包含元素[1,2,3],那么map(x -> x*2)的执行结果一定是[2,4,6]。反之,如果数据源没有定义元素的出现顺序,那么结果可能是[2,4,6]中元素的任何排列。
串行流(sequential streams)
使用sequential()
可以基于原始流的到一个串行流。对于串行流,不论流中元素的出现顺序是否已经定义好都不影响性能,只影响结果。如果流是有序的,对完全相同的流执行重复的操作会产生完全相同的结果;反之,重复的操作可能产生不同的结果。
并行流(parallel streams)
使用parallel()
方法可以基于原始流得到一个并行流。在并行流上的操作必须满足3个约束,以确保并行流上操作结果与串行流相同
- 无状态 (解释见上文)
- 不干预 (操作不会改变流)
- 关联性 (给定一个关联运算符,在一系列操作中使用该运算符,先处理哪一对操作数是无关紧要的)
对于并行流,解除顺序的限制有时可以提高执行效率,如某些聚合操作(aggregate operations)如筛选去重(distinct()
)和分组缩减(Collectors.groupingBy()
),如果操作与元素的顺序无关可以执行的更有效率。
而另一方面,对于与元素出现顺序相关联的操作,如limit(),为了保证顺序将会需要加入buffer,这会削弱并行对效率的提高。
但其实,对于大多数使用流进行管道操作(stream pipelines)的场景,即使流的元素是有序的,相较于串行操作,并行操作仍然是高效的。
无序流(unordered streams)
如果流的元素有定义好的出现顺序,但是使用者并不关心顺序的场景,使用unordered()
显式地将流转化为无序的将会提高某些中间操作的并发性能。
缩减操作
一个缩减操作(也称为折叠操作)通过重复的合并(combining operations)将一组输入元素合成一个总的结果, 比如在一组数字中找最大值或将他们求和,再比如将元素累加成一个list。Stream<T>
接口提供了通用的缩减操作,如reduce()
和collect()
,也提供了很多特殊的缩减操作如sum()
, max()
, count()
等。
你可能会问,要累加用下面这种简单的遍历就可以实现啊,要啥缩减操作?
int sum = 0;
for (int x : numbers) {
sum += x;
}
缩减操作除了可读性更高,更重要的是只要缩减操作满足无状态(stateless)和相关性(associative),它就可以并行的执行,这比上面那种串行的遍历性能好多了。用缩减操作重写上面的求和代码:
int sum = numbers.stream().reduce(0, (x,y) -> x+y);
由于满足无状态和相关性,可以直接在并行流上安全的执行:
int sum = numbers.parallelStream().reduce(0, Integer::sum);
能够很好的并行执行的原因是缩减操作可以对流中数据的多个子集同时操作,然后将中间结果合并为最终结果。
假设widgets是Widget对象的集合,Widget有一个getWeight方法,我们可以用如下代码求widgets的总重量:
OptionalInt sumOfWeights = widgets.parallelStream()
.mapToInt(Widget::getWeight)
.sum();
这是个典型的map-reduce应用,使用的是特殊缩减操作max()。缩减操作更通用的方法是
Stream<T>
接口提供的三个版本的reduce()
:
// 第一种
Optional<T> reduce(BinaryOperator<T> accumulator);
// 第二种
T reduct(T identity, BinaryOperator<T> accumulator);
// 第三种
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
我们直接介绍最复杂的第三种,它将类型为T的元素,缩减产生一个类型为U的结果,包含三个参数:
- identity 元素,既是缩减的初始操作元素(seed value),也是在流中没有元素时缩减的默认返回值。严格一些,identity的值必须保证,对于所有的元素u,
combiner.apply(identity,u)
结果还是u - accumulator函数,输入一个局部结果和下一个元素,输出一个新的局部结果
- combiner函数,将两个局部结果合并,产生一个新的局部结果。(combiner在并行缩减中是必须的,输入是分段累积计算的局部结果,输出是最终结果)。
使用reduce()实现上面widget的例子
int sumOfWeights = widgets.parallelStream()
.reduce(0,
(sum, b) -> sum + b.getWeight()),
Integer::sum);
对比上个示例中的map-reduce操作,可以看出通用的reduce()方法将映射(mapping)步骤合并到了累加(accumulating)步骤中。
映射操作(mapping)
上文中已经出现过映射操作,它是一种中间操作,对流中的没元素执行一些计算,然后把计算结果放入一个新流中。
相关的api 包括map()
和flatMap()