Java8 新特性之 Stream 使用指南
- 什么是 Stream
- Stream 的结构组成
- 获取 Stream 的方式
- Stream 和集合的区别
- 一些需要注意的问题
- 如何使用 Stream
- filter:返回与给定谓词相匹配的元素
- distinct:去除重复元素
- skip:跳过前 n 个元素。
- limit:返回前 n 个元素。
- anyMatch:Stream 中有任意一个元素与给定的谓语相匹配,返回 true。
- noneMatch:Stream 中的所有元素都与给定的谓语不匹配,返回 true。
- sort:排序
- map:通过给定的函数,将输入流中的元素映射到输出流并返回。
- flatMap:通过映射函数,作用到流中的每个元素,并组成返回成一个新的流。
- collect:将流还原成集合。
- reduce:汇聚操作,根据给定的累加器,将 Stream 中的元素一个个累加计算。进行什么操作与累加器相关,如相乘、相加、比较大小等。
- findFrist:返回第一个元素
- findAny:返回任意一个元素(串行流返回第一个元素),如果是空流,则返回empty Optional。
- max:返回流中的最大值
- min:返回流中的最小值
什么是 Stream
关于 Stream(流),官方文档给出的描述是:Classes to support functional-style operations on streams of elements, such as map-reduce transformations on collections.
翻译成中文也就是说:流是一个用于支持在元素流上进行函数式操作的类,例如集合上的map-reduce转换。它可以十分方便高效地实现聚合操作或大批量数据处理,且代码十分简洁。比如在一个彩色笔的集合中,求出红笔的重量总和,可以这么写:
Pen redPen1 = new Pen("red", 10);
Pen redPen2 = new Pen("red", 15);
Pen redPen3 = new Pen("red", 13);
Pen yellowPen1 = new Pen("yellow", 10);
Pen yellowPen2 = new Pen("yellow", 16);
List<Pen> pens = Arrays.asList(redPen1, redPen2, redPen3, yellowPen1, yellowPen2);
int sum = pens.stream()
.filter(p -> "red".equals(p.getColor()))
.mapToInt(p -> p.getWeight())
.sum();
System.out.println("sum: " + sum);
输出结果为:
sum: 38
可以看到,我们只用一句代码就实现了在一个集合中求取符合某个条件的数值总和。如果我们不使用 Stream 来实现,这将需要用大篇幅的代码来编写。
简单来说,流就是一个来自数据源的元素队列,能够对集合中的每个元素进行一系列并行或串行的流水线操作。
- 数据源:即流的来源, 如集合、数组等。如上面的示例中的 pens 集合。
- 元素队列:元素是特定类型的对象,形成一个队列。 值得注意的是,Java中的 Stream 不是数据结构,不会存储元素,它只与计算相关。如上面的示例中使用了 stream() 方法将 pens 转换成一个串行流。
- 聚合操作:类似SQL语句一样的操作, 如 filter、map、reduce、find、match、sorted等。如上面的示例中使用了 filter() 过滤取出颜色为 red 的笔,而后使用 mapToInt() 映射取出红笔的 weight,最后使用 sum() 求出红笔重量总和。
- 流的种类有:Stream、LongStream、IntStream、DoubleStream。每种流都可以选择串行或并行。默认是串行。
Stream 的结构组成
流操作分为中间操作(Intermediate)和终端操作(Terminal),并组合成流管道(stream pipelines)。其构成如下图所示:
中间操作:中间操作都是惰性化的,在执行终端操作之前,调用的中间操作都不会真正执行,而是返回一个新的流,一直到终端操作被调用。中间操作还可分为有状态(如 distinct()、sort())和无状态(如 map()、filter())。前者在执行过程中会保留先前看到的元素状态,而后者不会,且每个元素都可以独立于其他元素的操作进行处理。
终端操作:终端操作会产生一个结果或副作用。它总是饥饿的,会在返回之前,完成数据的遍历和处理(只有 iterator() 和 spliterator() 不是)。终端操作完成之后,流即失效,不能再使用。
获取 Stream 的方式
- 从集合中获取。如 Collection.stream()(串行流)、Collection.parallelStream()(并行流)。
- 从数组中获取。如 Array.Stream(Object[]) 。
- 从静态工厂方法中获取。如 Stream.of(Object[])、IntStream.range(int, int)、Stream.iterate(Object, UnaryOperator) 。
- 从文件中获取流。如 BufferedReader.lines() 。
- 其他方式,包括Random.ints()、BitSet.stream()、Pattern.splitAsStream(java.lang.CharSequence)、JarFile.stream()。
Stream 和集合的区别
不是数据结构。它只与计算相关,且按需计算,不存储任何数据。什么叫只与计算相关?用听音乐打个比方,音乐存储在硬盘上,需要时本地播放,这是集合;音乐存放在网络上,需要时从网络(数据源)获取,播放的音乐仍存放在原来的地方,这便是流。
功能性。流不会改变数据源。
惰性化。流的操作都是向后延迟的,当调用中间操作时,它并不会真正执行,而是等到终端操作被调用时,再合并一次性执行。关于中间操作和终端操作,后面会详细说明。
流可以无限大。短回路操作(如 limit()、findFirst())允许一个无穷大的流在有限的时间内返回计算结果。比如执行 limit(10) ,流在获取前 10 个元素后即返回,不再对后面的元素执行任何操作。
只能迭代一次。流和 Iterator 相似,都是只能迭代一次,必须重新生成流才能再次访问数据源。
一些需要注意的问题
- 流的参数一般是 lambda 表达式或方法引用。
- 不应在流的执行期间修改流的数据源。
- 尽量避免使用有状态的 lambda 表达式。
- 如果数据源不是有序的,则流也不保证有序性。可通过 unordered() 方法,声时可以无序。
- 慎重使用并行流。比如当需要保证有有序性时,使用并行流将可能破坏有序性。
如何使用 Stream
filter:返回与给定谓词相匹配的元素
仍以开篇中的基础数据做示例,返回重量大于 10 的笔并打印。
pens.stream().filter(pen -> pen.getWeight() > 10).forEach(pen -> System.out.println(pen));
结果如下:
Pen{color='red', weight=15}
Pen{color='red', weight=13}
Pen{color='yellow', weight=16}
可以看到,2 个 weight = 10 的元素已经被过滤掉了。
distinct:去除重复元素
需要注意是,distinct() 方法并不支持传递参数,因此使用时需要重写 equals() 和 hasCode() 方法。为了方便测试,这里我写的 equals() 方法并不判断颜色是否相同。即只要 weight 相等,equals() 就会返回 true。
pens.stream().distinct().forEach(pen -> System.out.println(pen));
结果如下:
Pen{color='red', weight=10}
Pen{color='red', weight=15}
Pen{color='red', weight=13}
Pen{color='yellow', weight=16}
可以看到,redPen1 和 yellowPen1 的 weight 都是 10,因此有一个被过滤掉了。但是 Stream 是如何确定要保留哪个元素呢?事实上,在顺序流中,distinct() 会保留重复元素中第一个出现的元素(parallelStream 也是如此),但如果流是无序的(如使用 unordered() 指明流是无序的,不需要保证稳定性),则返回结果也是不稳定的。
pens.stream().unordered().distinct().forEach(pen -> System.out.println(pen));
结果如下:
Pen{color='yellow', weight=16}
Pen{color='red', weight=13}
Pen{color='yellow', weight=10}
Pen{color='red', weight=15}
可以看到,此时保留的 weight = 10 的元素是 yellowPen1。我试了几次,有的时候也会返回 redPen1。
skip:跳过前 n 个元素。
pens.stream().skip(2).forEach(pen -> System.out.println(pen));
结果如下:
Pen{color='red', weight=13}
Pen{color='yellow', weight=10}
Pen{color='yellow', weight=16}
可以看到,返回的结果是跳过了前 2 个元素。此时如果是使用 parallelStream() 并行执行,一样会跳过前 2 个元素,但不能保证返回的结果的不稳定性,即每次执行,元素的顺序都有可能不同。
limit:返回前 n 个元素。
pens.stream().limit(3).forEach(pen -> System.out.println(pen));
结果如下:
Pen{color='red', weight=10}
Pen{color='red', weight=15}
Pen{color='red', weight=13}
可以看到,返回的结果是 list 的前 3 个元素。当然,limit 还可以和 skip 一起使用,返回从第 n 个元素开始,取 m 个元素。
pens.stream().skip(1).limit(2).forEach(pen -> System.out.println(pen));
结果如下:
Pen{color='red', weight=15}
Pen{color='red', weight=13}
可以看到,此时返回的恰好是第 2 个元素到第 3 个元素。
anyMatch:Stream 中有任意一个元素与给定的谓语相匹配,返回 true。
boolean isMatch = pens.stream().anyMatch(pen -> pen.getWeight() == 10);
System.out.println(isMatch);
结果如下:
true
allMatch:Stream 中的所有元素都与给定的谓语相匹配,返回 true。
boolean isMatch1 = pens.stream().allMatch(pen -> pen.getWeight() == 10);
System.out.println(isMatch1);
boolean isMatch2 = pens.stream().allMatch(pen -> pen.getWeight() > 0);
System.out.println(isMatch2);
结果如下:
false
true
noneMatch:Stream 中的所有元素都与给定的谓语不匹配,返回 true。
boolean isMatch1 = pens.stream().noneMatch(pen -> pen.getWeight() > 10);
boolean isMatch2 = pens.stream().noneMatch(pen -> pen.getWeight() > 20);
System.out.println(isMatch1);
System.out.println(isMatch2);
结果如下:
false
true
sort:排序
pens.stream().sorted(Comparator.comparing(Pen::getWeight)).forEach(pen -> System.out.println(pen));
System.out.println("===================");
pens.stream().sorted(Comparator.comparing(Pen::getWeight).reversed()).forEach(pen -> System.out.println(pen));
结果如下:
Pen{color='red', weight=10}
Pen{color='yellow', weight=10}
Pen{color='red', weight=13}
Pen{color='red', weight=15}
Pen{color='yellow', weight=16}
===================
Pen{color='yellow', weight=16}
Pen{color='red', weight=15}
Pen{color='red', weight=13}
Pen{color='red', weight=10}
Pen{color='yellow', weight=10}
sorted() 方法默认是自然排序,即从小到大。但是可以使用 reversed() 反转排序。
map:通过给定的函数,将输入流中的元素映射到输出流并返回。
还是以 原来的 list 做为示例,利用 map() 将 list 中的 weight 映射成一个流,再将其转换成 String 类型,并拼接成一个字符串。
String s = pens.stream().map(Pen::getWeight).map(item -> String.valueOf(item)).collect(Collectors.joining(" "));
System.out.println(s);
结果如下:
10 15 13 10 16
flatMap:通过映射函数,作用到流中的每个元素,并组成返回成一个新的流。
示例中通过 map() 将 list 中的 weight 映射成一个输出流后,将 weight 转换成 String 类型,切割字符串并打印。这里用了 2 种方法,一种是利用 map() 进行字符串切割后即返回;另一种是在切割后使用 flatMap() 映射多一次再打印。我们来看看二者有什么区别。
pens.stream().map(Pen::getWeight).map(item -> String.valueOf(item)).map(word -> word.split(" “)).forEach(System.out::println);
System.out.println(”==============");
pens.stream().map(Pen::getWeight).map(item -> String.valueOf(item)).map(word -> word.split(" ")).flatMap(Arrays::stream).forEach(System.out::println);
结果如下:
[Ljava.lang.String;@4dd8dc3
[Ljava.lang.String;@6d03e736
[Ljava.lang.String;@568db2f2
[Ljava.lang.String;@378bf509
[Ljava.lang.String;@5fd0d5ae
==============
10
15
13
10
16
可以看到,没有用 flatMap() 做多一次映射的,打印出只是一个地址。我们都知道,split() 方法返回的是一个 String[],我们直接去打印自然只能得到一个地址。而 flatMap() 可以将流中的内容返回,而不是返回一个流。
collect:将流还原成集合。
// List -> Stream -> List
List<Integer> penWeightList = pens.stream().map(Pen::getWeight).collect(Collectors.toList());
// List -> Stream -> Set
HashSet<Integer> penWeightSet = pens.stream().map(Pen::getWeight).collect(toCollection(HashSet::new));
// List -> Stream -> Double(计算平均值)
Double averagWeight = pens.stream().collect(averagingInt(Pen::getWeight));
penWeightList.forEach(System.out::println);
System.out.println("===========================");
penWeightSet.forEach(System.out::println);
System.out.println("===========================");
System.out.println(averagWeight);
结果如下:
10
15
13
10
16
===========================
16
10
13
15
===========================
12.8
reduce:汇聚操作,根据给定的累加器,将 Stream 中的元素一个个累加计算。进行什么操作与累加器相关,如相乘、相加、比较大小等。
// 一个参数(累加器)
Optional sum = pens.stream().map(Pen::getWeight).reduce((a, b) -> a + b);
System.out.println("sum: " + sum.get());
Optional max = pens.stream().map(Pen::getWeight).reduce((a, b) -> a > b ? a : b);
System.out.println("max: " + max.get());
// 两个参数(初始值,累加器)
int sum2 = pens.stream().map(Pen::getWeight).reduce(10, (a, b) -> a + b);
System.out.println("sum2: " + sum2);
// 三个参数(初始值,累加器, 组合器),第三个参数只在并行时生效
int sum3 = pens.stream().map(Pen::getWeight).reduce(10, (a, b) -> a + b, (a, b) -> a + b);
System.out.println("sum3: " + sum3);
int sum4 = pens.parallelStream().map(Pen::getWeight).reduce(10, (a, b) -> a + b, (a, b) -> a + b);
System.out.println("sum4: " + sum4);
结果如下:
sum: 64
max: 16
sum2: 74
sum3: 74
sum4: 114
可以看到,sum 和 max 分别计算出了 weight 的总和以及 weight 中的最大值。sum2 和 sum3 相等。这是因为在串行流中,reduce() 的第三个参数是不起作用的,而在并行流中,reduce() 的第三个参数会将各线程的计算结果组合起来。
在开篇的代码中,给定的 weight 有 10、15、13、10、16。在 reduce() 方法中我又给了初始值 10,因此在串行流中的计算应是 10 + 10 = 20,20 + 15 = 35,35 + 13 = 48,48 + 10 = 58,58 + 16 = 74。但是当有第三个参数且流并行执行时,它是这么计算执行的:10 + 10 = 20,10 + 15 = 25,10 + 13 = 23,10 + 10 = 20,10 + 16 = 26;20 + 25 + 23 + 20 + 26 = 114。
在前面我们已经学习到,流的一个特性就是向后延迟,在执行最终的操作之前都不会进行真正的计算,因此执行地,线程间互不影响,都是拿初始值进行加法运算,最后由组合器(第三个参数)组合返回。
findFrist:返回第一个元素
Optional firstEmelment = pens.stream().findFirst();
System.out.println(firstEmelment.get());
结果如下:
Pen{color='red', weight=10}
findAny:返回任意一个元素(串行流返回第一个元素),如果是空流,则返回empty Optional。
Optional optional = pens.stream().map(pen -> pen.getWeight()).findAny();
System.out.println(optional.get());
Optional optional1 = pens.parallelStream().map(pen -> pen.getWeight()).findAny();
System.out.println(optional1.get());
结果如下:
10
13
max:返回流中的最大值
Optional maxWeight = pens.stream().max(Comparator.comparing(Pen::getWeight));
System.out.println(maxWeight.get());
结果如下:
Pen{color='yellow', weight=16}
min:返回流中的最小值
Optional minWeight = pens.stream().max(Comparator.comparing(Pen::getWeight));
System.out.println(minWeight.get());
结果如下:
Pen{color='red', weight=10}
count:返回流中元素的个数
long count = pens.stream().distinct().count();
System.out.println(count);
结果如下:
4
参考文章
使用Java 8 Stream像操作SQL一样处理数据
Java 8 中的 Streams API 详解
Java 8新特性:全新的Stream API
Stream 官方文档