Java8 新特性之 Stream 使用指南

什么是 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)。其构成如下图所示:


流的构成.png

中间操作:中间操作都是惰性化的,在执行终端操作之前,调用的中间操作都不会真正执行,而是返回一个新的流,一直到终端操作被调用。中间操作还可分为有状态(如 distinct()、sort())和无状态(如 map()、filter())。前者在执行过程中会保留先前看到的元素状态,而后者不会,且每个元素都可以独立于其他元素的操作进行处理。
终端操作:终端操作会产生一个结果或副作用。它总是饥饿的,会在返回之前,完成数据的遍历和处理(只有 iterator() 和 spliterator() 不是)。终端操作完成之后,流即失效,不能再使用。

获取 Stream 的方式

  1. 从集合中获取。如 Collection.stream()(串行流)、Collection.parallelStream()(并行流)。
  2. 从数组中获取。如 Array.Stream(Object[]) 。
  3. 从静态工厂方法中获取。如 Stream.of(Object[])、IntStream.range(int, int)、Stream.iterate(Object, UnaryOperator) 。
  4. 从文件中获取流。如 BufferedReader.lines() 。
  5. 其他方式,包括Random.ints()、BitSet.stream()、Pattern.splitAsStream(java.lang.CharSequence)、JarFile.stream()。

Stream 和集合的区别

  1. 流不是数据结构。它只与计算相关,且按需计算,不存储任何数据。什么叫只与计算相关?用听音乐打个比方,音乐存储在硬盘上,需要时本地播放,这是集合;音乐存放在网络上,需要时从网络(数据源)获取,播放的音乐仍存放在原来的地方,这便是流。
  2. 功能性。流不会改变数据源
  3. 惰性化。流的操作都是向后延迟的,当调用中间操作时,它并不会真正执行,而是等到终端操作被调用时,再合并一次性执行。关于中间操作和终端操作,后面会详细说明。
  4. 流可以无限大。短回路操作(如 limit()、findFirst())允许一个无穷大的流在有限的时间内返回计算结果。比如执行 limit(10) ,流在获取前 10 个元素后即返回,不再对后面的元素执行任何操作。
  5. 只能迭代一次。流和 Iterator 相似,都是只能迭代一次,必须重新生成流才能再次访问数据源。

一些需要注意的问题

  1. 流的参数一般是 lambda 表达式或方法引用。
  2. 不应在流的执行期间修改流的数据源。
  3. 尽量避免使用有状态的 lambda 表达式。
  4. 如果数据源不是有序的,则流也不保证有序性。可通过 unordered() 方法,声时可以无序。
  5. 慎重使用并行流。比如当需要保证有有序性时,使用并行流将可能破坏有序性。

如何使用 Stream

  1. 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 的元素已经被过滤掉了。

  1. 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。

  1. 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 个元素,但不能保证返回的结果的不稳定性,即每次执行,元素的顺序都有可能不同。

  1. 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 个元素。

  1. anyMatch:Stream 中有任意一个元素与给定的谓语相匹配,返回 true。
 boolean isMatch = pens.stream().anyMatch(pen -> pen.getWeight() == 10);
System.out.println(isMatch);

结果如下:

true
  1. 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
  1. 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
  1. 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() 反转排序。

  1. 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
  1. 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() 可以将流中的内容返回,而不是返回一个流。

  1. 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
  1. 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。

在前面我们已经学习到,流的一个特性就是向后延迟,在执行最终的操作之前都不会进行真正的计算,因此执行地,线程间互不影响,都是拿初始值进行加法运算,最后由组合器(第三个参数)组合返回。

  1. findFrist:返回第一个元素
Optional firstEmelment = pens.stream().findFirst();
System.out.println(firstEmelment.get());

结果如下:

Pen{color='red', weight=10}
  1. 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
  1. max:返回流中的最大值
Optional maxWeight = pens.stream().max(Comparator.comparing(Pen::getWeight));
System.out.println(maxWeight.get());

结果如下:

Pen{color='yellow', weight=16}
  1. min:返回流中的最小值
 Optional minWeight = pens.stream().max(Comparator.comparing(Pen::getWeight));
System.out.println(minWeight.get());

结果如下:

Pen{color='red', weight=10}
  1. 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 官方文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容