关于 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 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 来实现,这将需要用大篇幅的代码来编写。
简单来说,流就是一个来自数据源的元素队列,能够对集合中的每个元素进行一系列并行或串行的流水线操作。
流操作分为中间操作(Intermediate)和终端操作(Terminal),并组合成流管道(stream pipelines)。其构成如下图所示:
中间操作:中间操作都是惰性化的,在执行终端操作之前,调用的中间操作都不会真正执行,而是返回一个新的流,一直到终端操作被调用。中间操作还可分为有状态(如 distinct()、sort())和无状态(如 map()、filter())。前者在执行过程中会保留先前看到的元素状态,而后者不会,且每个元素都可以独立于其他元素的操作进行处理。
终端操作:终端操作会产生一个结果或副作用。它总是饥饿的,会在返回之前,完成数据的遍历和处理(只有 iterator() 和 spliterator() 不是)。终端操作完成之后,流即失效,不能再使用。
不是数据结构。它只与计算相关,且按需计算,不存储任何数据。什么叫只与计算相关?用听音乐打个比方,音乐存储在硬盘上,需要时本地播放,这是集合;音乐存放在网络上,需要时从网络(数据源)获取,播放的音乐仍存放在原来的地方,这便是流。
功能性。流不会改变数据源。
惰性化。流的操作都是向后延迟的,当调用中间操作时,它并不会真正执行,而是等到终端操作被调用时,再合并一次性执行。关于中间操作和终端操作,后面会详细说明。
流可以无限大。短回路操作(如 limit()、findFirst())允许一个无穷大的流在有限的时间内返回计算结果。比如执行 limit(10) ,流在获取前 10 个元素后即返回,不再对后面的元素执行任何操作。
只能迭代一次。流和 Iterator 相似,都是只能迭代一次,必须重新生成流才能再次访问数据源。
仍以开篇中的基础数据做示例,返回重量大于 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() 方法并不支持传递参数,因此使用时需要重写 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。
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 个元素,但不能保证返回的结果的不稳定性,即每次执行,元素的顺序都有可能不同。
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 个元素。
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
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
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() 反转排序。
还是以 原来的 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
示例中通过 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() 可以将流中的内容返回,而不是返回一个流。
// List -> Stream -> List
List penWeightList = pens.stream().map(Pen::getWeight).collect(Collectors.toList());
// List -> Stream -> Set
HashSet 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
// 一个参数(累加器)
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。
在前面我们已经学习到,流的一个特性就是向后延迟,在执行最终的操作之前都不会进行真正的计算,因此执行地,线程间互不影响,都是拿初始值进行加法运算,最后由组合器(第三个参数)组合返回。
Optional firstEmelment = pens.stream().findFirst();
System.out.println(firstEmelment.get());
结果如下:
Pen{color='red', weight=10}
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
Optional maxWeight = pens.stream().max(Comparator.comparing(Pen::getWeight));
System.out.println(maxWeight.get());
结果如下:
Pen{color='yellow', weight=16}
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 官方文档