流合并
Stream API
,包括 filter、map、sorted 都统称为 聚合操作。
聚合操作就是把集合中的对象做整体性的计算。
一般来说,计算、操作、处理 这几个词都是表达的同一个意思,都是比较宽泛的含义。尤其是计算,不要以为仅仅是加减乘除。
小案例
对 1-10 的十个正整数求和:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
普通的使用 for
循环完成计算的代码实现是:
int sum = 0;
for (int i : numbers) {
sum += i;
}
System.out.println("sum : " + sum);
而使用 Stream API 完成计算的代码实现是:
import java.util.Arrays;
int sum = numbers.stream()
.reduce((a, b) -> a + b)
.get();
System.out.println("1-10求和 : " + sum);
reduce()
方法的作用,是合并了所有的元素,终止 计算出一个结果。注意这里终止的意思,就是流已经到达终点结束了,不能再继续流动了。
reduce()
方法的返回值是一个比较复杂的对象,需要调用 get()
方法返回最终的整数值。
同理,
get()
方法返回值的类型,也是系统自动根据流中元素类型推定的。
reduce()
方法的参数就稍微有点复杂了(重点理解):
a
在第一次执行计算语句a + b
时,指代流的第一个元素;然后充当缓存作用以存放本次计算结果。此后执行计算语句时,a
的值就是上一次的计算结果并继续充当缓存存放本次计算结果。b
参数第一次执行计算语句时指代流的第二个元素。此后依次指代流的每个元素。
注意:a、b 两个参数的作用是由位置决定的,变量名是任意的
结合下图理解:
reduce()
方法的第一个参数(本例的 a
)有多重作用,并且系统是自动完成参数(本例的 a, b
)赋值的,所以仍然体现了 Stream
编程的重点仍然是计算(本例的 a + b
)。
reduce()
方法也是可以操作对象的。
对于上节习题中的数据:
List<Student> students = new ArrayList<>();
students.add(new Student("赵祯", 92));
students.add(new Student("曹丹姝", 60));
... ...
... ...
计算三年二班的学生的分数,并在 Console 中打印本班级学生平均分。
如果需求比较复杂,可能简单的整数就不够了,就要使用 Student
对象:
Student result = students.stream()
.reduce(
(a, b) -> {
a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());
return a;
}
)
.get();
System.out.println(result.getName() + " - " + result.getMidtermScore());
Console 的输出结果是:
赵祯 - 777
这是 bug,如果计算后,再在网页上显示每位同学的分数,赵祯同学的分数就错误的显示成 777 分。
出现 bug 的主要原因是,第一个 Student
对象由于充当了缓存角色,正确性被破坏了。
解决办法
reduce()
提供了另一种参数形式,可以自己 new
一个对象充当缓存角色,而不是使用流中的原始对象。
Student result = students.stream()
.reduce(new Student("", 0),
(a, b) -> {
a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());
return a;
}
);
System.out.println(result.getName() + " - " + result.getMidtermScore());
reduce()
方法的参数变为了两个:
- 第一个参数,是作为缓存角色的对象
- 第二个参数,是 Lambda 表达式,完成计算,格式是一样的。
- 那么
a
变量不再指代流中的第一个元素了,专门指代缓存角色的对象,即方法第一个参数对象。 b
变量依次指代流的每个元素,包括第一个元素。a
、b
职责非常清晰了。
- 那么
对照下图理解 a
、b
参数的功能变化:
reduce()
方法的返回值同样发送了变化,返回作为缓存角色的对象,即第一个参数。
不用再调用一次
get()
方法了。
赵祯 - 92
- 777
流中的原始对象没有被破坏,在一个没有姓名的缓存对象中存放计算结果。没有 bug 了
小结
从 reduce()
的学习可以感受到,对于整个 Java 流 的设计,由于系统会自动做很多事情,所以在学习和理解时,知识点是比较隐晦的,逻辑也有点复杂。
但是一旦理解后,写代码就爽快很多,编程的焦点更加明确了。
可以说,特点是 “约定大于代码” 。Java 把规范制定好了,开发者只要专注于开发计算逻辑。
流收集
在实际工作中,整体功能如果比较复杂的话,使用流对集合进行计算后,可能并不想输出和合并,而是把结果元素放在一个新的集合中,待进一步使用。
例如,新的集合可以传递给 Thymeleaf 模板等等。
小习题:对于一组数字:
List<Integer> numbers = Arrays.asList(3, 2, 2, 7, 63, 2, 3, 5);
找出最大的前 3
个数字放入一个新的集合中,用 -
组合成字符串打印。
import java.util.stream.Collectors;
List<String> numResult = numbers.stream()
.sorted((n1, n2) -> n2 - n1)
.limit(3)
.map(a -> "" + a)
.collect(Collectors.toList());
String string = String.join("-", numResult);
System.out.println("字符串是: " + string);
collect()
方法的作用就是收集元素,但元素收集存放到哪去呢?Collectors.toList()
是一个静态方法,作为参数告诉 collect()
方法存入一个 List
集合。所以 collect()
方法的返回值类型就是 List
。
java.util.stream.Collectors
是流工具包中提供的收集器。
为了能够把最终结果转换为字符串打印,调用了 map()
方法把流中原来的整数映射为字符串("" + a
),所以 collect()
方法的返回值类型就是 List<String>
,而不是 List<Integer>
String.join()
的用法已经在《Java面向对象》课程中学过了
collect(Collectors.toList())
几乎是一个标准用法了。
并行流
我们回忆一下 Stream API
的设计,很像是一个管道:
管道的显著特点是,每个节点是依次执行的,下一个节点必须等待上一个节点执行完毕。这种执行方式,通常叫作 串行。
无论多少个节点都排成一个队伍
性能问题
如果计算过程越来越复杂、数据量越来越大,串行工作模式性能会越来越低。我们学习过程中所举的案例,数据量没那么大,这是为了便于理解。由于目前已经进入大数据时代,在实际工作中的数据,可能会数据量大、计算过程复杂。
串行工作模式的性能很难被优化。因为这种模式无法发挥 多核 CPU 的优势。
只有一个队伍,核再多也只能等待
解决办法
为了充分发挥 多核 CPU 的优势,可以把 串行 计算模式,改为 并行 计算模式。
当然,并行 计算模式不能简单的把一个队伍变成三五个队伍。
所谓并行,就是利用多线程,变成同时执行。多线程可以充分发掘多核 CPU 的优势。
小案例
对 500000~1 中的所有偶数,求平方值后进行自然排序。比较一下 串行 和 并行 的性能差异。
使用并行流的代码很简单,不再调用 stream()
方法,改为调用 parallelStream()
方法即可。其它的计算方法是一样的。
parallelStream()
以并行的方式执行任务,同时也支持流的收集、合并等计算。结合下图理解与串行运算的不同:
同样。并行流的计算过程被 Java 封装了,使用起来很简单。目前需要熟练应用即可。当然,如果任务拆分等过程和细节有兴趣,可以研究 Stream API 源码。
大家多运行几次,会看到每次耗时毫秒数都不完全相同,总体来说,还是 并行 计算模式耗时更短。
我们在《Java面向对象》第 8 章学习过使用
Duration.between()
方法计算两个时间的差值哦。duration.toMillis()
输出毫秒数
不适合使用并行计算的场景
流中的每个数据元素之间,有逻辑依赖关系的时候,不适合使用并行计算。
上面视频中的例子可以看到,数字输出的顺序不是期望中的 1 2 3 4 5
,而是无序的,而且多运行几次,顺序都可能不一样。
这是因为并行计算使用了多线程,每个线程独立输出数字,而线程的输出时机,是由 CPU 动态决定的,无法确定。
所以,逻辑上要求数字必须按书写的前后顺序(数字之间有逻辑顺序)输出时,就不能使用并行计算。
并行流的性能意外
实际上,并行 计算模式的性能 不是 任何情况下都优于 串行 计算模式。
场景的原因有两种:
一、硬件太差
CPU 核数很低,特别是单核情况下,并行 计算模式不一定更好。因为多线程也要等待 CPU 资源,不能很好的发挥多线程的优势。
二、任务简单
数据量小、任务简单的情况下,并行 计算模式不一定更好。因为多线程的管理也是会消耗CPU、内存等资源的,可能比任务本身的开销更大。
选择
我们必须先理解 串行 与 并行 两种模式的差别,然后根据实际情况选择使用。
由于实际情况中,硬件、需求复杂度等各种因素比较复杂,所以实际上没有确定的选择方案。
一般来说,任务执行超过一小时的情况下,考虑使用 并行 模式优化性能。
任务执行时间较短,又没有特别要求,使用 串行 模式也问题不大。
对任务有实时性要求,希望立即得到计算结果,最好是比较一下两种模式,比如各运行 100 次比较使用哪种模式最好。
使用 并行 要特别注意是否会造成数据之间是否有逻辑关系,出 bug 就不好了。