文章目录
1. Stream 简介
集合
和数组
是经常会用到的数据结构,通常会对它们做增、删、改、查、聚合、统计、过滤等操作。在较早的 Java 版本中对集合和数组的处理并不是很便捷,不过Java 8
添加了一个新的抽象Stream
,可以以一种声明的方式处理数据。Stream
使用一种类似用 SQL
语句从数据库查询数据的直观方式来提供对 Java 集合运算和表达的支持,这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等
Java 源码中有文档介绍流相关信息,可参考
java/util/stream/package-info.java
,读者如对 Stream 原理感兴趣可参考 Java 8 Stream(2)-原理解析
Stream 的特性如下
:
无存储
Stream不是一种数据结构,只是数据源的一个视图,数据源可以是一个数组,Java容器或 I/O channel 等为函数式编程提供支持
对 Stream 的任何修改都不会修改真实的数据源,比如 Stream 的过滤操作并不会删除过滤掉的元素,而是产生一个不包含被过滤元素的新 Stream懒加载思想
Stream 的中间操作并不会立即执行,只有等到用户真正需要结果(最终操作触发)的时候才会执行可消费性
Stream只能被最终操作
操作一次,之后就会失效,类似容器的迭代器,想要再次操作必须重新生成。在最终操作之后,再次使用流将抛出异常:java.lang.IllegalStateException: stream has already been operated upon or closed
对于流的处理主要有三种关键性操作:流的创建、中间操作(intermediate operation)以及最终操作(terminal operation)
2. Stream 创建
创建 Stream
流通常有以下几种方法:
-
使用已有集合创建流
在Java 8
中对集合类做了增强,在其中增加了stream
方法,可以将一个集合类转换成流。以下代码通过一个已有的List
调用其stream()
方法创建一个流,这种创建流的方式也是最常用的一种除此以外还有一个
parallelStream()
方法可以为集合创建一个并行流
。parallelStream()
采用多线程来计算,底层使用 ForkJoinPool 线程池,默认的并发线程数要比CPU处理器的数量少1个,因为最优的策略是每个 CPU 处理器分配一个线程,然而主线程也算一个线程// 1. 集合类转化 Arrays.asList("Nathan", "Hello", "HelloWorld"); Stream<String> stream = strings.stream(); // 2. 数组转化 Stream<String> stream = Arrays.stream(new String[]{"Nathan", "Hello", "HelloWorld"})
-
通过 Stream 创建流
使用Stream
提供的方法可以直接返回一个由指定元素组成的流Stream<String> stream = Stream.of("Nathan", "Hello", "HelloWorld");
3. Stream 常用操作
Stream
的操作主要分为两类:中间操作
和终结操作
。中间操作
得到的结果还是一个Stream
,我们需要借助终结操作
将一个 Stream
转换成想要的类型
3.1 Stream 常用中间操作
操作方法 | 功能 |
---|---|
filter | 将符合指定条件的元素过滤出来 |
map | 以指定的方式将元素映射为另一种类型的数据,其 lambda 表达式必须有返回值 |
flatMap | 与 map 类似,但当其操作的元素是仍然可迭代的数组或集合时,flatMap 会将可迭代数据结构中的元素取出,以指定方式映射后合并为一个流 |
distinct | 对流中的元素去重 |
sorted | 对流进行排序 |
limit | 返回 Stream 的前面 n 个元素 |
skip | 扔掉前 n 个元素 |
3.2 Stream 常用终结操作
操作方法 | 功能 |
---|---|
foreach | 遍历流中的每个元素 |
count | 统计流中的元素个数 |
collect | 归约操作,可将流中的元素组成一个汇总结果,常用参数Collectors.toList() |
max | 统计流中最大对元素,但需要指定元素比较的规则 Comparator |
4. 使用示例
以下所有示例基于 People
对象,其定义如下
public class People {
// 姓名
private String name;
// 年龄
private Integer age;
}
4.1 分组并映射获取对象指定字段
假设有一个 People
对象集合,需要将其按照年龄分组,并且只保留分组后每个年龄分组中的姓名,参考示例如下:
// 1. 使用 Collectors.collectingAndThen()
Map<Integer, List<String>> ageNameMap = peopleList.stream()
.collect(Collectors.groupingBy(People::getAge,
Collectors.collectingAndThen(Collectors.toList(),
peoples -> peoples.stream().map(People::getName).collect(Collectors.toList()))));
// 2. 使用 Collectors.mapping()
Map<Integer, List<String>> ageNameMap = peopleList.stream()
.collect(Collectors.groupingBy(People::getAge,
Collectors.mapping(People::getName, Collectors.toList())));
4.2 分组取符合指定条件的最大值
假设需要将 People
对象集合按照年龄分组,并且取该分组下最长的名字,一个参考示例如下:
Map<Integer, String> ageNameMap = peopleList.stream()
.collect(groupingBy(People::getAge,
Collectors.reducing("", People::getName, BinaryOperator.maxBy(Comparator.comparing(String::length)))));