Java8 Stream 日常使用总结
一、概述
Stream是Java8提供的一种处理集合的强大工具,可以将Stream理解为一个处理集合数据的管道,在这个管道的各个部分,开发人员可以进行各种操作(如筛选、映射、查找和匹配、排序等),最终得到自己所需的数据。
1.1 无Stream vs Stream
以一个业务场景为例:设备模型中除了有产品型号这一固定属性分组,还有其他的用户自定的属性分组,现在需要获取某个模型中除产品型号之外的所有分组名称
List<Field> fields = fieldDao.findByModelId(modeldId);
Set<String> fieldsSet = new HashSet<>();
for (Field field : fields) {
// 1、获取不为产品型号的所有属性
if (!"产品型号".equals(field.getDisplay())) {
// 2、获取属性分组并将其加入到Set中去重
fieldsSet.add(field.getAttributeGroup());
}
}
// 3、将Set转换为List
List<String> result = new ArrayList<>(fieldsSet);
因为需求相对简单,Stream之前的处理集合的方式看起来不算十分复杂,但是这段代码最大的问题在于可读性,阅读者需要仔细研读代码,才能意会到这段代码到底在做什么。
而使用Stream后,代码可以这样写:
List<Field> fields = fieldDao.findByModelId(modeldId);
List<String> group = fields.stream()
.filter(item -> !"产品型号".equals(field.getDisplay())) // 过滤得到非"产品型号"的属性
.map(item -> c.getAttributeGroup()) // 映射属性的分组信息
.distinct() // 去重
.collect(Collectors.toList());
使用Stream后,代码更加简洁,并且代码可读性大大提高,这对于后期的维护是很有帮助的
1.2 Stream优点
概括起来,使用Stream处理集合有以下三个优点:
-
声明式——更简介,更易读
使用Stream处理集合数据时,处理逻辑易读,业务意图明显
-
可复合——更灵活
Stream完全是一种组合式的操作,可以将各种操作进行任意组合,类似于数学中的高阶函数
-
可并行——性能更优
提供ParallelStream,无需开发者写任何的多线程代码即可使用多核优势加速计算
二、Stream简单介绍
使用流一般包括三个必要步骤
- 构建流
- 组织中间操作链
- 使用终端操作
2.1 构建流
2.1.1 从集合生成流
日常开发构建流的最常用方式就是从已有集合生成流,Java8中Collection
类中增加了stream()
和parallelStream()
方法,可以直接将集合转换为串行流和并行流。
// list是一个已有的集合对象
list.stream();
list.parallelStream();
2.1.2 由值创建流
可以使用静态方法 Stream.of()
,通过显式值创建一个流,它可以接受任意数量的参数。
Stream<String> stream = Stream.of("Java", "8", "Strem");
2.1.3 由数组创建流
可以使用静态方法 Arrays.stream()
从数组创建一个流,它接受一个数组作为参数。
String[] numbers = {
"1", "2", "3", "4", "5"};
Stream<String> stream = Arrays.stream(numbers);
2.1.4 由文件生成流
Java中用于处理文件等I/O操作的 NIO API 已更新,以便利用 Stream API。java.nio.file.Files 中的很多静态方法都会返回一个流。例如,一个很有用的方法是 Files.lines()
,它会返回一个由指定文件中的各行构成的字符串流。
Stream<String> stream = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());
2.1.5 由函数生成流:创建无限流
Stream API 提供了两个静态方法来从函数生成流:Stream.iterate()
和 Stream.generate()
。这两个操作可以创建所谓的无限流。由 iterate 和generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去。一般来说,应该使用 limit(n)
来对这种流加以限制,以避免打印无穷多个值。
// 偶数无限流
Stream<Integer> stream = Stream.iterate(0, n -> n + 2);
// 生成五个0到1之间的随机双精度数
Stream<Double> stream = Stream.generate(Math::random)
.limit(5);
2.2 中间操作
2.2.1 概览
得到流对象以后,可以进行多个中间操作,中间操作会返回另一个流,使得多个操作可以连接起来形成一个流水线。
操作 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
---|---|---|---|
filter | Stream | Predicate | T -> boolean |
distinct | Stream | ||
map | Stream | Function<T, R> | T -> R |
flatMap | Stream | Function<T, Stream> | T -> Stream |
sorted | Stream | Comparator | (T, T) -> int |
skip | Stream | long | |
limit | Stream | long | |
takeWhile(Java 9,切片) | Stream | Predicate | T -> boolean |
dropWhile(Java 9,切片) | Stream | Predicate | T -> boolean |
表格字段说明:
-
操作
中间操作方法名称
-
返回类型
流的中间操作返回的还是流
-
使用的类型/函数式接口
中间操作接收的参数是何类型或是何函数式接口,如果是函数式接口,则可以使用Lambda表达式书写
-
函数描述符
函数描述符是函数式接口的抽象方法的签名,也即Lambda表达式的签名。例如,Predicate 接口中的方法签名是
boolean test(T t)
,则其Lambda表达式的签名即为 T -> boolean,也就是函数描述符,其中 -> 符号前面的T代表方法的入参,符号后面的 boolean代表方法的返回值。参照函数描述符,我们可以使用Java8引入的Lambda表达式简化我们的代码。
2.2.2 map 和 flatmap
-
map(Function<T, R>)
map
操作是将流中的元素进行再次加工形成一个新流。这在开发中很有用。比如我们有一个学生集合,我们需要从中提取学生的年龄以分析学生的年龄分布曲线。放在 Java 8 之前 我们要通过新建一个集合然后通过遍历学生集合来消费元素中的年龄属性。现在我们通过很简单的流式操作就完成了这个需求。
对应的代码为:
List<Integer> ages=studentList.stream().map(Student::getAge).collect(Collectors.toList());
-
flatMap(Function<T, Stream<R>>)
如果把上面的例子改一下,学生信息是以班级为单位组织的,gradeList 的类型是
List<List<Student>>
,这时要提取所有班级下,所有学生的年龄,再使用上面的map()
方法就行不通了List<List<Student>> studentGroup = gradeList.stream().map(Grade::getStudents).collect(Collectors.toList());
通过上面的操作,我们只能得到每个班的学生集合的集合
List>
。 我们还需要嵌套循环才能获取学生的年龄数据,十分不便。如果我们能返回全部学生的集合List
就方便多了。flatMap
可以解决这个问题!// flatMap 提取 List<Students> map 提取年龄 List<Integer> ages = gradeList.stream() .flatMap(grade -> grade.getStudents().stream()) .map(Student::getAge) .collect(Collectors.toList());
如上述代码所示,使用
flatMap
将所有的学生汇聚到一起。然后再使用map
操作提取年龄。flatMap
不同于map
地方在于map
只是提取属性放入流中,而 flatMap 先提取属性放入一个比较小的流,然后再将所有的流合并为一个流,有一种 “聚沙成塔” 的感觉。<