Java中Stream相关常用总结记录
阅读本篇文章之前最好有lambda表达式和方法引用
基础
什么是流?
首先贴一下比较官方的流的说法:
- 不存储数据:流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
- 函数式编程:流的操作不会修改数据源,例如filter不会将数据源中的数据删除
- 延迟操作: 惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际的计算。
- 可以解绑:对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n) 或 findFirst(),这些操作可是实现"短路"(Short-circuiting),访问到有限的元素后就可以返回。
- 纯消费:流的元素只能访问一次,类似iterator,操作没有回头路,如果你想从头访问一遍流的元素,那必须重新生成一个流。
流的操作是以管道方式串起来的,流管道包含一个数据源,接着包含0-n个中间操作,最后包含一个终点操作结束。
可能说的不是很专业,但我会尽量描述清楚,而且比较通俗易懂,应该不会有太大的偏差。
所谓流,我所理解的就是一种很舒服的针对集合容器中数据读取、操作的方式,且提供了函数式编程的方式,话说,在java8出现之前,没有这个概念,但是还是不影响我们操作数据,只是可能会不够“优雅”罢了,这里说的优雅,是相对的,因为有时候这种“优雅”反而会适得其反。
如何创建一个流?
Java中创建一个流很简单,只需调用集合对象的parallelStream()
或者stream()
,前者是并行流,后者是普通流。
所谓并行流,就是开启了多线程执行,一般不推荐使用,因为不好控制并发安全
List<Integer> list = new ArrayList<>();
//普通流
list.stream();
//并行流
list.parallelStream();
本文接下来默认使用普通流创建
如何关闭一个流?
既然我们知道了如何创建一个流,那么如何将流关闭呢,也就是结束掉流,毕竟我们的目的是想要通过流来帮我们操作集合中的元素,但我们操作完成后想获取结果,这时候流也就终止了。
我们可以看到,但我们创建一个流之后,返回的其实也是一个流对象,它是泛型的。
List<Integer> list = new ArrayList<>();
Stream<Integer> stream = list.stream();
我们可以使用以下方式终止流,来获取到我们想要的数据。
注:在后面我会详细说明各种方法的使用常见
-
查找与匹配
方法名 描述 allMatch(Predicate p) 检查是否匹配所有元素(当流中没有任何元素,会返回true) anyMatch(Predicate p) 检查是否至少匹配一个元素(当流中没有任何元素,会返回和false) noneMatch(Predicate p) 检查是否没有匹配所有元素(当流中没有任何元素,会返回和false) findFirst() 返回第一个元素 findAny() 返回当前流中的任意元素 max(Comparator c) 返回流中最大值 min(Comparator c) 返回流中最小值 forEach(Consumer c) 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了) -
归约
方法名 描述 reduce(T iden, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 T reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 Optional< T > -
收集
对应是
collect(Collectors.xxx())
方法方法名 描述 toList 把流中元素收集到List toSet 把流中元素收集到Set toCollection 把流中元素收集到创建的集合 counting 计算流中元素的个数 summingInt 对流中元素的整数属性求和 averagingInt 计算流中元素Integer属性的平均值 summarizingInt 收集流中Integer属性的统计值。如:平均值 joining 连接流中每个字符串 maxBy 根据比较器选择最大值 minBy 根据比较器选择最小值 reducing 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值 collectingAndThen 包裹另一个收集器,对其结果转换函数 groupingBy 根据某属性值对流分组,属性为K,结果为V partitioningBy 根据true或false进行分区
常用流的使用
一个集合转另一个集合(泛型不同)
这是我最经常用的方法
比如我这里有个学生类集合,我只需要他们的学号集合,我们就可以利用map
方法
map方法可以理解为映射,可以根据需求将原有的流类型统一映射为另外的类型
注意:在流中的操作都是依次的,它会针对集合中的所有元素依次操作
首先创建一个学生类
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Student {
private Integer num;
private String name;
private Integer age;
private String className;
}
将学生类集合转换学生学号集合
List<Student> studentList = new ArrayList<>();
studentList.add(new Student(1, "周杰伦", 42, "三年二班"));
studentList.add(new Student(2, "林俊杰", 42, "三年二班"));
//原始学生集合
System.out.println(studentList);
//利用学生集合创建流,然后把学生流重新映射为学生类里面的"Num"也就是Integer流
Stream<Integer> integerStream = studentList.stream().map(student -> {
return student.getNum();
});
//当然上面的写法可以简化为:
Stream<Integer> integerStreamSimplify = studentList.stream().map(Student::getNum);
//别忘了操作完流后,我们需要关闭流再将它转换成需要的数据
//将Integer流转换为Integer集合
List<Integer> studentNumList = studentList.stream().map(Student::getNum).collect(Collectors.toList());
别忘了
collect(Collectors.toList())
方法的意思是结束流,同时把流转换为对应的List集合。
将泛型为List的集合平铺(转成一个)
想象一个给你一个List<List<Integer>>
类型的集合,你需要把它转换为List<Integer>
类型的集合
你可能首先想到这样做:
List<List<Integer>> doubleIntegerList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
doubleIntegerList.forEach(integerList::addAll);
这样做确实可以,但个人总觉得不太优雅,因为要额外创建一个集合来一个个的addAll
如果我们使用流的flatMap
来做就不需要额外事先创建一个集合了
flatMap其实是对应map的,上面说到map是从一个对象流映射到另一个对象流,而如果你的对象流本身也可以生成流,就可以通过它将你生成的对象流统一收集
List<List<Integer>> doubleIntegerList = new ArrayList<>();
List<Integer> result = doubleIntegerList.stream().flatMap(integerList -> {
//返回一个流
return integerList.stream();
}).collect(Collectors.toList());
值得一提的是,这种平铺集合的方式使用流的效率在极端情况下其实并不高,但是它让我们的代码更简洁(可能也少了一些可读性)
下面是上面两种方法性能的对比:
可以看到,在有1000*10000
规模的条件下,用流的方式效率低了差不多有5倍
将集合转为Map
看似不起眼,实际上用熟练了非常好用的方法。
有时候需要遍历集合,然后再遍历过程中查询数据,这样会频繁的查询数据库,非常影响性能,所以推荐提前查询所有可能用得到的数据。
我最常用的使用场景就是作为批量查询数据缓存,通常是利用id
作为key
,方便后续直接取用.
List<Student> studentList = new ArrayList<>();
Map<Integer, Student> studentMap = studentList.stream().collect(Collectors.toMap(student -> {
return student.getNum();
}, student -> student));
toMap()
方法需要两个参数,它们都可以是lambda表达式,第一个为key
,不能重复,第二个为value
以上代码实际上可以简写为下面的形式
//模拟一次性批量从数据库查询所有数据
List<Student> studentList = new ArrayList<>();
Map<Integer, Student> studentMap = studentList.stream().collect(Collectors.toMap(Student::getNum, Function.identity()));
Function.identity()
函数表示当前对象
将集合分组
与上面的集合转map相对应的就是当你的key
不唯一的时候就可以使用groupingBy()
方法来实现分组操作。
//模拟一次性批量从数据库查询所有数据
List<Student> studentList = new ArrayList<>();
//按照班级将学生分组
Map<String, List<Student>> studentListMap = studentList.stream().collect(Collectors.groupingBy(Student::getClassName));
利用流排序
对于Student
对象,如果一个需求是想要先通过学生的年龄排序,再通过学号排序
//模拟一次性批量从数据库查询所有数据
List<Student> studentList = new ArrayList<>();
studentList.stream().sorted(Comparator.comparing(Student::getAge).thenComparing(Student::getNum)).collect(Collectors.toList());
值得一提的是,Comparator.comparing()
是比较器的一种封装,你可以传入所有实现了比较器的对象进行排序,默认是升序。thenComparing
可以再前面排序基础上再排序。
当然我们也可以自定义比较器对Student
进行排序
studentList.stream().sorted((student1, student2) -> {
//下面可以根据自己的需求对Student对象指定排序,返回1、-1、0分别代表大于、等于、小于
if (student1.getAge() > student2.getAge()) {
return 1;
} else if (student1.getAge() < student2.getAge()) {
return -1;
} else {
return 0;
}
}).collect(Collectors.toList());
关于Java中比较器可以看我另一篇文章:关于Java中的比较器
利用流做计算
List<Integer> integerList = new ArrayList<>();
//第一种方法,我们可以将对象流转换为:int、long、double流,然后调用sum()方法求和
int sum = integerList.stream().mapToInt(e -> e).sum();
//如果不用‘int、double、long’基础流的sum()方法,我们可以用reduce()方法来计算
Integer reduce = integerList.stream().reduce(0, (a, b) -> a + b);
reduce()
方法是一种聚合函数,它可以通过某种规则将所有流聚合成一个结果
一个容易被忽略的方法
有时候我们需要在处理过程中对流中的数据做一些处理,但又不想要结束流。
比如我想把学生信息先保存到数据库,再计算所有学生的年龄总和
我们就可以利用peek()
一步到位,倘若使用forEach()
,就会导致流结束。
peek()
与forEach()
唯一区别就是peek()
不会结束流