暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【JDK 8】Stream 进阶篇

风尘博客 2021-12-10
555

上一篇介绍了 Stream
的基础,本文作为上篇文章的补充,讲讲高阶玩法。

一、并行流

1.1 概念

上文介绍了 Stream
常用的两种创建方式,分别是【通过集合/数组创建】、【通过值创建】,这两种创建流的方式,也称为【顺序流】。

  • 顺序流

由主线程按顺序对流执行操作。

  • 并行流

把一个内容分成多个数据块,并用不同的线程分成多个数据块,并用不同的线程分别处理每个数据块的流。

例如:parallelStream
是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求

1.2 并行流

  • 直接创建
List<String> flatMapList = Arrays.asList("风尘_博客", "微信_公众号");
Stream<String> parallelStream = flatMapList.parallelStream();

顺序流转换为并行流

Stream<String> stream = flatMapList.stream();
Stream<String> parallelStreamAnother = stream.parallel();

1.3 两者区别

以筛选集合中的奇数的过程图,展示两者的区别

1.4 高效使用并行流

把顺序流转成并行流轻而易举,但却不一定是好事。

  1. 留意装箱。自动装箱和拆箱操作会大大降低性能。Java 8 中有原始类型流(IntStream
    LongStream
    DoubleStream
    )来避免这种操作,但凡有可能都应该用这些流;

  2. 有些操作本身在并行流上的性能就比顺序流差。特别是 limit
    findFirst
    等依赖于元素顺序的操作,它们在并行流上执行的代价非常大。例如, findAny
    会比 findFirst
    性能好,因为它不一定要按顺序来执行。

  3. 要考虑流的操作流水线的总计算成本。设 N
    是要处理的元素的总数,Q
    是一个元素通过 流水线的大致处理成本,则 N*Q
    就是这个对成本的一个粗略的定性估计。Q
    值较高就意味着使用并行流时性能好的可能性比较大。

  4. 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。并行处理少数几个元素 的好处还抵不上并行化造成的额外开销。

  5. 要考虑流背后的数据结构是否易于分解。例如,ArrayList
    的拆分效率比 LinkedList
    高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。

  6. 还要考虑终端操作中合并步骤的代价是大是小(例如 Collector
    中的 combiner
    方法)

二、流的一些高阶玩法

2.1 groupingBy 的多级分组

在介绍终止操作的 collect
中的一些使用时,举例中有【groupingBy 将学生按班级分组】,即我们所谓的一级分组。

  • 一级分组
// 将学生按班级分组
Map<String, List<StudentDomain>> group1 = studentDomainList.stream()
.collect(Collectors.groupingBy(per -> per.getClassNum()));
System.out.println(group1);

{一班={科学=[StudentDomain{id=6, name='小6', subject='科学', score=93.0, classNum='一班'}], 数学=[StudentDomain{id=2, name='小2', subject='数学', score=97.0, classNum='一班'}, StudentDomain{id=4, name='小4', subject='数学', score=95.0, classNum='一班'}]},
二班={科学=[StudentDomain{id=3, name='小3', subject='科学', score=96.0, classNum='二班'}], 数学=[StudentDomain{id=1, name='小1', subject='数学', score=98.0, classNum='二班'}, StudentDomain{id=5, name='小5', subject='数学', score=94.0, classNum='二班'}]}}

  • 二级分组
// 将学生先按班级分组,再按学科分组
Map<String, Map<String, List<StudentDomain>>> group2 = studentDomainList.stream().collect(Collectors.groupingBy(StudentDomain::getClassNum, Collectors.groupingBy(StudentDomain::getSubject)));
System.out.println(group2);

{一班={
科学=[StudentDomain{id=6, name='小6', subject='科学', score=93.0, classNum='一班'}],
数学=[StudentDomain{id=2, name='小2', subject='数学', score=97.0, classNum='一班'}, StudentDomain{id=4, name='小4', subject='数学', score=95.0, classNum='一班'}]},
二班={
科学=[StudentDomain{id=3, name='小3', subject='科学', score=96.0, classNum='二班'}],
数学=[StudentDomain{id=1, name='小1', subject='数学', score=98.0, classNum='二班'}, StudentDomain{id=5, name='小5', subject='数学', score=94.0, classNum='二班'}]}}

更多分级,以此类推。

2.2 toMap 的 key 重复时, 新旧 value 如何取舍

  • 普通 toMap
List<StudentDomain> studentDomainList = assStudent();
// toMap 取出学生名字和分数,组成map
Map<String, String> toMap = studentDomainList.stream()
.collect(Collectors.toMap(
// 取学生姓名为key
item -> item.getName(),
// 取分数为value
item -> String.valueOf(item.getScore())));
System.out.println(toMap);

此时由于 key
没有重复的,所以 Map
size()
跟原 List 相同。

{小2=97.0, 小1=98.0, 小6=93.0, 小5=94.0, 小4=95.0, 小3=96.0}

  • duplicate key
// 以班级为科目为 key 取出分数就会报错
Map<String, String> errorMap = studentDomainList.stream()
.collect(Collectors.toMap(
// 取学生姓名为key
item -> item.getSubject(),
// 取分数为value
item -> String.valueOf(item.getScore())));

就会报错,说明处理到已存在的 key
,其对应 value
98.0

java.lang.IllegalStateException: Duplicate key 98.0

解决方式就在提供的 Collectors.toMap()
方法中,其第三个参数就是当出现 duplicate key
的时候的处理方案。

  • 【方案一】:出现重复时,取前面 value
    的值,或者取后面放入的 value
    值,覆盖先前的 value
Map<String, String> map1 = studentDomainList.stream()
.collect(Collectors.toMap(
item -> item.getSubject(),
item -> String.valueOf(item.getScore()), (v1, v2) -> v1));

  • 【方案二】:Map 的 value 可以储存一个 list,把重复 key 的值放入 list,再存到 value 中
Map<String, List<String>> map2 = studentDomainList.stream()
.collect(Collectors.toMap(
StudentDomain::getSubject,
e -> Arrays.asList(String.valueOf(e.getScore())),
(List<String> oldList, List<String> newList) -> {
oldList = Stream.concat(oldList.stream(), newList.stream()).collect(Collectors.toList());
return oldList;
}));
System.out.println(map2);

这里网上有个【大坑】,不少人都抄过来抄过来抄过去,写出:

List<StudentDomain> studentDomainList = assStudent();
Map<String, List<String>> map2 = studentDomainList.stream()
.collect(Collectors.toMap(
StudentDomain::getSubject,
e -> Arrays.asList(String.valueOf(e.getScore())),
(List<String> oldList, List<String> newList) -> {
oldList.addAll(newList);
return oldList;
}));
System.out.println(map2);

区别就在于拼接 List
用的是 addAll()
,小伙伴可以拿去执行一下,肯定会报 java.lang.UnsupportedOperationException

简单科普一下:我们的 oldList
newList
都是使用 Arrays.asList()
创建的,都是长度不可变,当你想使用 addAll()
方法,当然会出现java.lang.UnsupportedOperationException

当然,我这里用 Stream
流转了一下,如果你的项目中使用了 guava
的话,可以将 oldList
newList
Lists.newArrayList()
创建,然后再用 addAll()
方法拼接,也是可以的。

三、性能测试

Stream API
可以极大提高 Java
程序员的生产力,让程序员写出高效率、干净、简洁的代码。Stream API
的性能到底如何呢,代码整洁的背后是否意味着性能的损耗呢?

详见:Stream Performance[1]

放一下别人的测试结果:

  1. 对于简单操作,比如最简单的遍历,Stream
    串行 API
    性能明显差于显示迭代,但并行的 Stream API
    能够发挥多核特性。
  2. 对于复杂操作,Stream
    串行 API
    性能可以和手动实现的效果匹敌,在并行执行时 Stream API
    效果远超手动实现。

所以,如果出于性能考虑,

  1. 对于简单操作推荐使用外部迭代手动实现;
  2. 对于复杂操作,推荐使用 Stream API
  3. 在多核情况下,推荐使用并行 Stream API
    来发挥多核优势;
  4. 单核情况下不建议使用并行 Stream API
[1]

https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/8-Stream%20Performance.md

文章转载自风尘博客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论