Java基础学习(二)-集合流式编程
一、集合流的简介
1.1、集合流式编程的简介
Stream是JDK8后出现的新特性,也是JDK8中最值得学习的两种新特性之一(另一个是Lambda表达式)。
Stream是对集合操作的增强,流不是集合的元素,不是一种数据结构,不负责数据存储,流更像是一个迭代器,可以单向遍历一个集合中的每一个元素,并且不可循环。
1.2、为什么要使用集合流式编程?
有些时候,对集合中的元素进行操作的时候,需要使用到其他操作的结果,在这个过程中,集合的流式据程可以大幅度的简化代码的数量。将数据源中的数据,读取到一个流中,可以对这个流中的数据进行操作(删除、过滤,映射..…)。每次的操作结果也是一个流对象,可以对这个流再进行其他的操作。
1.3、使用步骤
通常情况下,对集合中的数据使用流式编程,需要经过以下三步:
- 获取数据源,将数据源中的数据读取到流中
- 对流中的数据进行各种各样的处理。
- 对流中的数据进行整合处理。
在上述三个过程中,过程2中,有若干方法,可以对流中的数据进行各种各样的操作,并且返回流对象本身,这样的操作,被称为中间操作,过程3中,有若干方法,可以对流中的数据进行各种处理,并关闭流,这样的操作,被称为最终操作。
在中间操作和最终操作中,基本上所有的方法参数都是函数式接口,可以使用lambda表达式来实现,使用集合的流式编程,来简化代码量,是需要对lambda表达式做到熟练掌握。
二、数据源的获取
2.1、数据源简介
数据源,顾名思义,既是流中的数据的来源。是集合的流式编程的第一步,将数据源中的数据读取到流中,进行处理。
注意:将数据读取到流中进行处理的时候,与数据源中的数据没有关系。也就是说,中间操作对流中的数据进行处理、过滤、映射、排序..,此时是不会影响数据源中的数据的。
2.2、数据源获取
这个过程,其实是将一个容器中的数据,读取到一个流中,因此无论什么容器作为数据源,读取到流中的方法返回值一定是一个Stream对象。
1、集合作为数据源
- 调用集合对象的stream()方法,这个方法会返回一个Stream对象。
这里以ArrayList对象为例
1 | //1 实例化一个集合 |
运行程序,查看结果
- 调用集合对象的parallelStream()方法,这个方法会返回一个Stream对象。
区别:stram()方法获取的是一个同步流,而parallelStream()方法返回一个并发流。
2、数组作为数据源
- 调用数据工具类 Arrays 的 stream() 方法,传入转换为流对象的数组
1 | //1 实例化一个数组 |
运行程序
- 将基本数据类型数组转换为流对象,同样调用数据工具类 Arrays 的 stream() 方法,会根据传入数组的类型返回对应的流对象。
这里以int类型数组为例
1 | //1 实例化一个数组 |
三、最终操作
首先编写一个getStream静态方法,这个方法用于将一个整数集合转换为一个Stream对象
1 | public static Stream<Integer> getStream() { |
3.1、简介
将流中的数据整合到一起,可以存入一个集合,也可以直接对流中的数据进行遍历、数据统计.…,通过最终操作,需要掌握如何从流中提取出来我们想要的信息。
注意事项:最终操作,之所以叫最终操作,是因为,在最终操作执行结束后,会关闭这个流,流中的所有数据都会销毁,如果使用一个已经关闭了的流,会出现异常。
3.2、collect
将流中的数据收集到一起,对这些数据进行一些处理,最常见的处理,就是将流中的数据存入一个集合,collect方法的参数,是一个Collector接口,而且这个接口并不是一个函数式接口,实现这个接口,可以自定义收集的规则,但是,绝大部分情况下,不需要自定义。
直接使用
Collectors
工具类提供的方法即可。
- 将流中的数据转成List
1 | List<Integer> list = new ArrayList<>(); |
运行结果如下
- 将流中的数据转成一个Set
1 | List<Integer> list = new ArrayList<>(); |
运行结果如下
- 将流中的数据转换为一个Map,分别实现键的生成规则和值的生成规则
1 | List<Integer> list = new ArrayList<>(); |
运行结果
3.3、reduce
将流中的数据以一定的规则聚合起来
reduce方法需要传入一个 BinaryOperator
函数式接口对象,这个接口是BiFunction的子接口
在 BinaryOperator 接口中,又对 BiFunction 中的泛型做了进一步限制,故 BinaryOperator 的apply方法中参数和返回值都是一个类型
- 使用reduce函数实现元素相加
1 | //1 读取数据源,得到流对象 |
上面代码的执行逻辑为:
将集合第一二个元素作为p1、p2,然后将得到的结果作为p1,第三个元素作为p2,再次执行函数,以此类推
运行程序,查看结果
3.4、count
统计流中的元素数量
1 | long count = getStream().count(); |
执行程序,查看结果
3.5、foreach
foreach方法需要传入一个 Consumer函数接口对象,该接口中有一个accept抽象方法,accept方法接受一个参数,且没有返回值
1 |
|
遍历流中的元素
1 | getStream().forEach(ele -> { |
配合方法引用,以上代码可以简化为:
1 | getStream().forEach(System.out::println); |
3.6、max&min
获取流中的最大元素、最小元素
Stream类中 max 和 min 方法源码如下
1 | Optional<T> max(Comparator<? super T> comparator); |
max 和 min 方法需要传入一个 Comparator 接口对象,该接口中有一个compare方法,用于自定义大小比较规则
1 |
|
获取流中最大值最小值的代码如下,传入Integer类中的compareTo方法
1 | Integer max = getStream().max(Integer::compareTo).get(); |
Integer类中实现了Comparator方法,重写了compareTo方法
1 | public int compareTo(Integer anotherInteger) { |
运行程序,查看结果
3.7、Matching
- allMatch:只有当流中所有的元素,都匹配指定的规则时,才会返回true
1 | boolean result = getStream().allMatch(ele -> ele > 50); |
运行程序,查看结果
- anyMatch:只要流中有任意数据满足指定规则,就会返回true
1 | boolean result = getStream().anyMatch(ele -> ele % 2 == 0); |
运行程序,查看结果
- noneMatch:只有当流中的所有元素,都不满足指定的规则时,才会返回true
1 | boolean result = getStream().noneMatch(ele -> ele > 50); |
查看结果
3.8、find
1、findFirst
从流中获取一个元素(获取开头的元素)
1 | Integer integer = getStream().findFirst().get(); |
运行程序,查看结果
2、findAny
从流中获取一个元素(一般情况下,是获取的开头的元素)
1 | Integer integer = getStream().findAny().get(); |
运行程序
3、说明
上面的两个方法,在绝大部分情况下,是完全相同的,但是在多线程的环境下,
findFirst
和findAny
返回的结果可能不一样。 findAny在并行流中获取的可能不是首元素
3.9、IntStream额外的最终操作
1、准备一个数组,转换为IntStream
1 | public static IntStream getIntStream() { |
2、获取数据最大值
1 | System.out.println(getIntStream().max().getAsInt()); |
3、获取数据最小值
1 | System.out.println(getIntStream().min().getAsInt()); |
4、获取数组和
1 | System.out.println(getIntStream().sum()); |
5、获取流中数据数量
1 | System.out.println(getIntStream().count()); |
6、获取流中所有数据的平均值
1 | System.out.println(getIntStream().average().getAsDouble()); |
7、获取到一个对流中数据的分析结果
1 | IntSummaryStatistics statistics = getIntStream().summaryStatistics(); |
- 获取最大值
1 | System.out.println("最大值为:" + statistics.getMax()); |
- 获取最小值
1 | System.out.println("最小值为:" + statistics.getMin()); |
- 获取数据和
1 | System.out.println("和为:" + statistics.getSum()); |
- 获取平均值
1 | System.out.println("平均值为:" + statistics.getAverage()); |
3.10、注意事项
在执行完以上的最终操作后,流将会被关闭,流中所有数据都会被销毁,如果去操作一个已经关闭的流会抛出异常。
四、中间操作
创建一个静态内部类Student,构造函数、Getter/Setter和toString省略不写
1 | private static class Student { |
编写一个getDataSource方法,这个方法用于返回一个Stream对象
1 | public static Stream<Student> getDataSource() { |
4.1、简介
将数据从数据源中读取到流中,中间操作,就是对流中的数据进行各种各样的操作、处理,中间操作可以连续操作,每一个操作的返回值都是一个Stream对象,可以继续进行其他操作,直到操作结束。
4.2、filter
条件过滤,仅保留满足指定条件的数据,其他不满足的数据都会被剔除。
Stream类中 filter 方法如下,这个方法要求传入一个Predicate接口对象
1 | Stream<T> filter(Predicate<? super T> predicate); |
Predicate接口中的抽象方法如下
1、过滤掉集合中成绩不及格的学生
筛选条件为
学生成绩 >= 60
1 | Stream<Student> stream = getDataSource(); |
运行程序,查看结果
2、过滤掉集合中未成年的学生
筛选条件为
学生年龄 >= 18
1 | Stream<Student> stream = getDataSource(); |
运行程序
4.3、distinct
去除集合中重复的元素,这个方法 没有 参数,去重的规则与HashSet相同。
先比较hashCode,如果hashCode结果相同,那么使用equals方法比较
- 重写Student类中的equals和hashCode方法
1 |
|
- 修改数据源,添加重复数据
1 | public static Stream<Student> getDataSource() { |
- 使用Stream类中的distinct方法去重
1 | Stream<Student> stream = getDataSource(); |
- 运行程序,查看结果
4.4、sorted
Stream类中有两个sorted方法,一个有参,一个无参
1 | Stream<T> sorted(); |
1、有参sorted
有参 sorted 方法需要我们传入一个Comparator接口对象,我们需要自定义比较大小规则。
- 在调用sorted时传入一个Comparator接口对象,自定义比较大小规则
1 | Stream<Student> stream = getDataSource(); |
- 查看结果
2、无参sorted
将流中的数据,按照其对应的类实现的Comparable接口提供的比较规则进行排序。
- 让Student类实现Comparable接口,重写compareTo方法
1 | private static class Student implements Comparable<Student>{ |
- 根据 Student 中 compareTo 方法定义的规则进行排序
1 | Stream<Student> stream = getDataSource(); |
- 查看结果
4.5、limit & skip
limit
:限制,截取流中指定数量的元素
skip
:跳过,跳过流中指定数量的元素
1、limit
获取成绩前五名的学生信息
- 修改学生类中的compareTo方法
1 |
|
- 代码如下
1 | getDataSource().sorted().limit(5).forEach(System.out::println); |
- 查看结果
2、skip
跳过集合中的前五个学生,从第六个学生开始打印
1 | getDataSource().sorted().skip(5).forEach(System.out::println); |
查看结果
3、limit&skip
输出集合中成绩在第三-第七的学生信息,使用limit和skip
1 | getDataSource().sorted().skip(2).limit(5).forEach(System.out::println); |
运行,查看结果
4.6、map&flatMap
1、map
map:提供一个映射规则,对流中的数据进行映射,用新的数据替换旧的数据
Stream类中map的源码如下
1 | <R> Stream<R> map(Function<? super T, ? extends R> mapper); |
其中Function接口是一个传入一个参数,返回一个返回值的函数式接口
实际需求:获取所有学生的名字
1 | Stream<Student> stream = getDataSource(); |
运行程序,查看结果
上面的代码也可以使用Student类中的方法引用来实现
1 | Stream<Student> stream = getDataSource(); |
2、flatMap
flatMap:扁平化映射,一般用于map映射后,流中的数据是一个容器,而我们需要对容器中的数据进行处理,此时我们可以使用扁平化映射,将容器中的数据直接读取到流中。
- 模拟:统计字符串数组中所有出现的字符
思路如下:
- 将字符串数组中的每一个字符串都切割为一个字符串数组
- 将字符串数组中的数据进行扁平化映射
- 对流中的字符串使用distinct进行去重
- 打印
1 | String[] array = {"Hello","World","Hehe"}; |
- 运行程序,查看结果
五、Collectors工具类
5.1、概念
Collectors 是一个工具类,里面封装了很多方法,可以很方便的获取到一个Collector接口的实现类对象,从而可以使用collect方法,对流中的数据进行各种各样的处理和整合。
5.2、常用方法
方法 | 描述 |
---|---|
Collectors.toList() | 将流中的数据,整合到一个 List 集合中 |
Collectors.toSet() | 将流中的数据,整合到一个 Set 集合中 |
Collectors.toMap() | 将流中的数据,整合到一个 Map 集合中 |
maxBy() | 按照指定的规则,找到流中最大的元素,等同与 max |
minBy() | 按照指定的规则,找到流中最小的元素,等同与 min |
joining() | 将流中的数据拼接为一个字符串,注意:只能操作流中是 String 的数据 |
summingInt() | 将流中的数据映射成 int 类型的数据,并求和 |
averagingInt() | 将流中的数据映射成 int 类型的数据,并求平均值 |
summarizingInt() | 将流中的数据映射成 int 类型的数据,并获取描述信息 |
5.3、示例代码
1、joining
- 拼接字符串数组中的所有字符串
1 | //1 准备一个字符串数组,作为数据源 |
运行程序,查看结果
重载方法一
- 传入一个分隔符,令拼接而成的字符串根据传入的分隔符分割开来,格式为:
字符串
分隔符
字符串2分隔符
字符串3 …
- 传入一个分隔符、一个前缀和一个尾缀,拼接而成的结果字符串格式为
“前缀” 字符串
分隔符
字符串2分隔符
字符串3 … “后缀”
- 传入分隔符的joining
1 | //1 准备一个字符串数组,作为数据源 |
运行程序,查看结果
- 传入分隔符、前缀和尾缀的joining
1 | //1 准备一个字符串数组,作为数据源 |
结果
2、summingInt
Collectors 中 summingInt 方法的源码如下
summingInt方法需要传入一个ToIntFunction接口对象 ,这个接口的抽象方法将一个对象转换为int类型数据
1 |
|
- 输出字符串数组中的字符串总长度
1 | //1 准备一个字符串数组,作为数据源 |
结果
六、综合案例
一个集合中存储了若干个学生对象,要求编码实现以下功能
其中,学生类的属性如下
1 |
|
数据源如下
1 | public static Stream<Student> getStream() { |
6.1、所有及格的学生信息
使用中间操作
filter
,筛选条件为学生成绩 >= 60
1 | Stream<Student> stream = getStream(); |
结果
6.2、所有及格的学生姓名
使用中间操作
filter
和map
,其中筛选条件为学生成绩>=60
,在筛选完及格的学生后,需要使用map
将学生对象映射为 学生姓名
1 | Stream<Student> stream = getStream(); |
结果
6.3、所有学生的平均成绩
用到流对象
mapToInt
方法,这个方法将Stream对象转换为一个IntStream对象,再调用 IntStream 对象的average
和getAsDouble
方法即可。
1 | Stream<Student> stream = getStream(); |
结果
6.4、班级的前三名
先使用Stream对象的
sorted
方法排序,然后使用limit
取前三条即可。
1 | Stream<Student> studentStream = getStream(); |
结果
6.5、班级的3-10名
先使用Stream对象的
sorted
方法排序,然后使用skip
跳过前三条,再用limit
取后面七条即可
1 | Stream<Student> studentStream = getStream(); |
结果
6.6、所有不及格学生的平均成绩
先使用
filter
筛选所有不及格学生,然后使用mapToInt
将流对象转换为IntStream流对象,最后调用IntStream流对象的average
和getAsDouble
方法即可。
1 | Stream<Student> stream = getStream(); |
结果
6.7、将及格的学生,按照降序顺序输出所有信息
先使用
filter
筛选所有及格学生,然后使用sorted
进行排序,再输出所有及格学生信息即可
1 | Stream<Student> stream = getStream(); |
结果
6.8、班级学生的总分
使用流对象的
mapTo
方法,将流对象转换为IntStream对象,然后调用sum
方法即可。
1 | Stream<Student> stream = getStream(); |
结果如下