再聊Java Stream的一些实战技能与注意点

这篇具有很好参考价值的文章主要介绍了再聊Java Stream的一些实战技能与注意点。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

大家好,又见面了。

在此前我的文章中,曾分2篇详细探讨了下JAVA中Stream流的相关操作,2篇文章收获了累计 10w+阅读、2k+点赞以及 5k+收藏的记录。能够得到众多小伙伴的认可,是技术分享过程中最开心的事情。

  • 吃透JAVA的Stream流操作,多年实践总结
  • 讲透JAVA Stream的collect用法与原理,远比你想象的更强大

不少小伙伴在评论中提出了一些的疑问或自己的独到见解,也在评论区中进行了热烈的互动讨论。梳理了下相关评论内容,针对此前文章中没有提及的一些典型讨论点拿出来聊一聊,也是作为对此前两篇Java Stream相关文章内容的补充完善。

Stream处理时列表到底循环了多少次

看下面这段Stream使用的常见场景:

Stream.of(17, 22, 35, 12, 37)
        .filter(age -> age > 18)
        .filter(age -> age < 35)
        .map(age -> age + "岁")
        .collect(Collectors.toList());

在这段代码里面,同时有2个 filter操作和1个 map操作以及1个 collect操作,那么这段代码执行的时候,究竟是对这个list执行了几次循环操作呢?是每一个Stream步骤都会进行一次遍历操作吗?为了验证这个问题,我们将上述代码改写一下,打印下每个步骤的结果:

        List<String> ages = Stream.of(17,22,35,12,37)
                .filter(age -> {
                    System.out.println("filter1 处理:" + age);
                    return age > 18;
                })
                .filter(age -> {
                    System.out.println("filter2 处理:" + age);
                    return age < 35;
                })
                .map(age -> {
                    System.out.println("map 处理:" + age);
                    return age + "岁";
                })
                .collect(Collectors.toList());

先执行,得到如下的执行结果。其实结果已经很明显的可以看出,stream流处理的时候,是对列表进行了一次循环,然后顺序的执行给定的stream执行语句。

按照上述输出的结果,可以看出其处理的过程可以等价于如下的常规写法:

        List<Integer> ages = Arrays.asList(17,22,35,12,37);
        List<String> results = new ArrayList<>();
        for (Integer age : ages) {
            if (age > 18) {
                if (age < 35) {
                    results.add(age + "岁");
                }
            }
        }
        System.out.println(results);

所以,Stream并不会去遍历很多次。其实上述逻辑也符合Stream 流水线加工的整体模式,试想一下,一条流水线上分环节加工一件商品,同一件产品也不会在流水线上加工2次的吧~

Stream究竟是让代码更易读还是更难懂

Java8引入了 Lambda函数式接口Stream等新鲜内容以来,针对使用Stream或Lambda语法究竟是让代码更易懂还是更复杂的争议,一直就没有停止过。有的同学会觉得Stream语法的方式,一眼就可以看出业务逻辑本身的含义,也有一些同学认为使用了Stream之后代码的可读性降低了很多。

其实,这是个人编码模式与理念上的不同感知而已。Stream主打的就是让代码更聚焦自身逻辑,省去其余繁文缛节对代码逻辑的干扰,整体编码上会更加的简洁。但是刚接触的时候,难免会需要一定的适应期。技术总是在不断迭代、不断拥抱新技术、不去刻意排斥新技术,或许是一个更好的选项。

那么,话说回来,如何让自己能够一眼看懂Stream代码、感受到Stream的简洁之美呢?分享个人的一个经验:

  1. 先了解几个常见的Stream的api的功能含义(Stream的API封装的很优秀,很多都是字面意义就可以理解)
  2. 改变意识,聚焦纯粹的业务逻辑本身,不要在乎具体写法细节

下面举了个例子,如何用上述的2条方法,快速的让自己理解一段Stream代码表达的意思。

那么上面这段代码的含义就是,先根据员工子公司过滤所有上海公司的人员,再获取员工工资最高的那个人信息。怎么样?按照这个方法,是不是可以发现,Stream的方式,确实更加容易理解了呢~

在IDEA中debug调试Stream代码段

技术分享其实是一个双向的过程,分享的同时,也是自我学习与提升的机会,除了可以梳理发现一些自己之前忽略的知识点并加以巩固,还可以在互动的时候get到新的技能。

比如,我在此前的 Java Stream介绍的文章中,有提过基于Stream进行编码的时候会导致代码 debug调试的时候会比较困难,尤其是那种只有一行Lambda表达式的情况(因为如果代码逻辑多行编写的时候,可以在代码块内部打断点,这样其实也可以进行debug调试)。

关于这一点,很多小伙伴也有相同的感受,比如下面这个评论:

你以为这就结束了?接下来一个小伙伴的提示,“震惊”了众人!纳尼?原来Stream代码段也是可以debug单步调试的?

跟踪Stream中单步处理过程的操作入口按钮长这样:

并且,另一个小伙伴补充说这是IDEA2019.03版本开始有的功能:

嗯?难怪呢,我一直用的2019.02版本的,所以才没用上这个功能(强行给自己找了个台阶、哈哈哈)。于是,我悄悄的将自己的idea升级到了最新的2023.02版本(PS:新版本的UI挺好看,就是bug贼多)。好啦,言归正传,那么究竟应该如何利用IDEA来实现单步DEBUG呢?一一起来感受下吧。

在代码行前面添加断点的时候,如果要打断点的这行代码里面包含Stream中间方法map\filter\sort之类的)的时候,会提示让选择断点的具体类型

一共有三种类型断点可供选择:

  • Line:断点打在这一行上,不会进入到具体的Stream执行函数块中
  • Lambda:代码打在内部的lambda代码块上
  • Line and Lambda:代码走到这行或者执行这一行具体的函数块内容的时候,都会进入断点

下面这个图可以更清晰的解释清楚上述三者的区别。一般来说,我们debug的时候,更多的是关注自身的业务具体逻辑,而不会过多去关注Stream执行框架的运转逻辑,所以大部分情况下,我们选择第二个Lambda选项即可

按照上面所述,我们在代码行前面添加一个Lambda类型断点,然后debug模式启动程序执行,等到断点进入的时候便可以正常的进行debug并查看内部的处理逻辑了。

如果遇到图中这种只有一行的lambda形式代码,想要看下返回值到底是什么的,可以选中执行的片段,然后 ALT+F8打开Evaluate界面(或者右键选择 Evaluate Expression),点击 Evaludate按钮执行查看具体结果。

大部分情况下,掌握这一点,已经可以应付日常的开发过程中对Stream代码逻辑的debug诉求了。但是上述过程偏向于细节,如果需要看下整个Stream代码段整体层面的执行与数据变化过程,就需要上面提到的Stream Trace功能。要想使用该功能,断点的位置也是有讲究的,必须要将断点打在stream开流的地方,否则看不到任何内容。另外,对于一些新版本的IDEA而言,这个入口也比较隐蔽,藏在了下拉菜单中,就像下面这个样子。

我们找到Trace Current Stream Chain并点击,可以打开Stream Trace界面,这里以chain链的方式,和stream代码块逻辑对应,分步骤展示了每个stream处理环节的执行结果。比如我们以 filter环节为例,窗口中以左右视图的形式,左侧显示了原始输入的内容,右侧是经过filter处理后符合条件并保留下来的数据内容,并且还有连接线进行指引,一眼就可以看出哪些元素是被过滤舍弃了的:

不止于此,Stream Trace除了提供上述分步查看结果的能力,还支持直接显示整体的链路执行全貌。点击Stream Trace窗口左下角的 Flat Mode按钮即可切换到全貌模式,可以看到最初原始数据,如何一步步被处理并得到最终的结果。

看到这里,以后还会说Stream不好调试吗?至少我不会了。

小心Collectors.toMap出现key值重复报错

在我们常规的HashMap的 put(key,value)操作中,一般很少会关注key是否已经在map中存在,因为put方法的策略是存在会覆盖已有的数据。但是在Stream中,使用 Collectors.toMap方法来实现的时候,可能稍不留神就会踩坑。所以,有小伙伴在评论区热心的提示,在使用此方法的时候需要手动加上 mergeFunction以防止key冲突。

这个究竟是怎么回事呢?我们看下面的这段代码:

public void testCollectStopOptions() {
    List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(22));
    // collect成HashMap,key为id,value为Dept对象
    Map<Integer, Dept> collectMap = ids.stream()
            .collect(Collectors.toMap(Dept::getId, dept -> dept));
    System.out.println("collectMap:" + collectMap);
}

执行上述代码,不出意外的话会出意外。如下结果:

Exception in thread "main" java.lang.IllegalStateException: Duplicate key Dept{id=22}
	at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
	at java.util.HashMap.merge(HashMap.java:1254)
	at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
	at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)

因为在收集器进行map转换的时候,由于出现了重复的key,所以抛出异常了。 为什么会出现异常呢?为什么不是以为的覆盖呢?我们看下源码的实现逻辑:

可以看出,默认情况下如果出现重复key值,会对外抛出IllegalStateException异常。同时,我们看到,它其实也有提供重载方法,可以由使用者自行指定key值重复的时候的执行策略:

所以,我们的目标是出现重复值的时候,使用新的值覆盖已有的值而非抛出异常,那我们直接手动指定下让toMap按照我们的要求进行处理,就可以啦。改造下前面的那段代码,传入自行实现的 mergeFunction函数块,即指定下如果key重复的时候,以新一份的数据为准:

    public void testCollectStopOptions() {
        List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(22));
        // collect成HashMap,key为id,value为Dept对象
        Map<Integer, Dept> collectMap = ids.stream()
                .collect(Collectors.toMap(
                        Dept::getId,
                        dept -> dept,
                        (exist, newOne) -> newOne));
        System.out.println("collectMap:" + collectMap);
    }

再次执行,终于看到我们预期中的结果了:

collectMap:{17=Dept{id=17}, 22=Dept{id=22}}

By The Way,个人感觉JDK在这块的默认实现逻辑有点不合理。虽然现在默认的抛异常方式,可以强制让使用端感知并去指定自己的逻辑,但这默认逻辑与map的put操作默认逻辑不一致,也让很多人都会无辜踩坑。如果将默认值改为有则覆盖的方式,或许会更符合常理一些 —— 毕竟被广泛使用的HashMap的源码里,put操作默认就是覆盖的,不信可以看HashMap源码的实现逻辑:

慎用peek承载业务处理逻辑

peekforeach在Stream流操作中,都可以实现对元素的遍历操作。区别点在与peek属于中间方法,而foreach属于终止方法。这也就意味着peek只能作为管道中途的一个处理步骤,而没法直接执行得到结果,其后面必须还要有其它终止操作的时候才会被执行;而foreach作为无返回值的终止方法,则可以直接执行相关操作。

那么,只要有终止方法一起,peek方法就一定会被执行吗?非也看版本、看场景! 比如在 JDK1.8版本中,下面这段代码中的peek方法会正常执行,但是到了 JDK17中就会被自动优化掉而不执行peek中的逻辑:

    public void testPeekAndforeach() {
        List<String> sentences = Arrays.asList("hello world", "Jia Gou Wu Dao");
        sentences.stream().peek(sentence -> System.out.println(sentence)).count();
    }

至于原因,可以看下JDK17官方API文档中的描述:

因为对于 findFirstcount之类的方法,peek操作被视为与结果无关联的操作,直接被优化掉不执行了。所以说最好按照API设计时预期的场景去使用API,避免自己给自己埋坑。

我们从peek的源码的注释上可以看出,peek的推荐使用场景是用于一些调试场景,可以借助peek来将各个元素的信息打印出来,便于开发过程中的调试与问题定位分析。

我们再看下peek这个词的含义解释:

既然开发者给它起了这么个名字,似乎确实仅是为了窥视执行过程中数据的变化情况。为了避免让自己踩坑,最好按照设计者推荐的用途用法进行使用,否则即使现在没问题,也不能保证后续版本中不会出问题。

字符串拼接明明有join,那么Stream中Collectors.join存在意义是啥

在介绍Stream流的收集器时,有介绍过使用 Collectors.joining来实现多个字符串元素之间按照要求进行拼接的实现。比如将给定的一堆字符串用逗号分隔拼接起来,可以这么写:

    public void testCollectJoinStrings() {
        List<String> ids = Arrays.asList("AAA", "BBB", "CCC");
        String joinResult = ids.stream().collect(Collectors.joining(","));
        System.out.println(joinResult);
    }

有很多同学就提出字符串元素拼接直接用 String.join就可以了,完全没必要搞这么复杂。

如果是纯字符串简单拼接的场景,确实直接String.join会更简单一些,这种情况下使用Stream进行拼接的确有些大材小用了。 但是 joining的方法优势要体现在Stream体系中,也就是与其余Stream操作可以结合起来综合处理。String.join对于简单的字符串拼接是OK的,但是如果是一个Object对象列表,要求将Object某一个字段按照指定的拼接符去拼接的时候,就力不从心了——而这就是使用 Collectors.joining的时机了。比如下面的实例:

小结

好啦,关于Java Stream相关的内容点的补充,就聊到这里啦。如果需要全面了解Java Stream的相关内容,可以看我此前分享的文档。那么,你对Java Stream是否还有哪些疑问或者自己的独特理解呢?欢迎一起交流下。

传送门:

  • 吃透JAVA的Stream流操作,多年实践总结
  • 讲透JAVA Stream的collect用法与原理,远比你想象的更强大

我是悟道,聊技术、又不仅仅聊技术~

如果觉得有用,请点赞 + 关注让我感受到您的支持。也可以关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。文章来源地址https://www.toymoban.com/news/detail-707954.html

到了这里,关于再聊Java Stream的一些实战技能与注意点的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 集群部署项目时,Spring Task的坑大家一定要注意

    我们要在下面的代码中,实现每5秒钟执行一个打印信息的任务。 首先我们可以通过“Edit Configurations”,增加一个运行实例的配置。 接着启动项目的多个实例即可,如下图所示: 然后我们就可以查看上面代码的执行结果了,如下图: 通过对比执行结果,我们会发现,有两个

    2024年02月12日
    浏览(37)
  • 微软官宣裁员 10000 人。分享一些我的建议给大家

    大家好!我是韩老师。 昨天,西雅图双雄经历着不眠之夜。 早些时间,就有传言说 1 月 18 日,亚马逊会裁员 18000 人。微软要裁员的各种消息也是满天飞。 北京时间昨天晚上,微软官方博客发了一篇标题为 Focusing on our short- and long-term opportunity 的文章。 其中,提及了裁员的

    2024年02月10日
    浏览(45)
  • 买电脑主要看哪些配置?有什么需要注意的?给大家推荐几款性价比高的电脑

    电脑是我们日常生活和工作中不可缺少的工具,但是面对市场上琳琅满目的电脑品牌和型号,我们该如何选择一台适合自己需求的电脑呢?本文将从以下几个方面介绍买电脑主要看哪些配置,有什么要注意的,以及推荐几个性价比高的电脑。 一般来说,买电脑主要看以下四个

    2024年02月12日
    浏览(37)
  • 经实测利用POSTMAN根本无法进行并发测试,大家不要再被一些搬运工给误导了

    以下为我的实测记录 一、先上我测试的接口代码,就是一个redis的tryLock分布式锁的获取,接口在获取到锁后,线程sleep了5秒,此时线程是不释放锁的,那按道理第二个请求在这个时间进来,是获取不到锁的,但结果却不是这样的 二、按照网上的那些博文,postman操作步骤如下

    2024年02月11日
    浏览(48)
  • Matlab绘图中的一些技能

    目录 1、matlab坐标轴设置多种字体(复合字体) 2、matlab图片中title生成的标题转移至图像下端 3、指定对应格式和期望dpi的图像进行保存、以及不留白保存 4、设置字体字号(x、y轴,标题。全局字体等) 5、设置刻度值信息,只有左、下有边框等 6、指定x、y轴刻度值 7、利用插值

    2023年04月26日
    浏览(27)
  • 2022全国职业技能大赛私有云搭建及注意事项

    环境准备 软件包: 答题云主机环境;CentOS-7-x86_64-DVD-2009.iso(centos7.9) iaas版本;chinaskills_cloud_iaas_v2.0.3.iso(openstack T版) 国赛竞赛方式: 1,openstack平台,现场会提供一个IP地址,登录用户名和密码,可以登录openstack平台,再此平台中,选手可以创建云主机来进行答题,相关

    2024年02月04日
    浏览(38)
  • 【LeetCode刷题篇零】一些基础算法知识和前置技能(上)

    冒泡排序 冒泡的核心是两两比较,大数下沉,小数上浮,比较的轮数是数组的长度 N,每一轮比较的次数为 N - 当前轮的索引: 外层循环控制轮数 round: [1,N] 内层循环控制次数 i: [0,N - round) 在每一轮当中,内循环中两两比较相邻的两个数,大数下沉(交换),如果某一轮没

    2024年02月07日
    浏览(46)
  • 项目架构一些注意点

    考虑系统的 1、如何解决那些不稳定的因素/问题?也是常说的如何容错。 2、一个系统的高可用取决于它本身和其强依赖的组件的高可用 3、消除单点 保活机制 健康检查 注册中心如何保障稳定性 注册中心集群 微服务本身对注册信息的本地持久化 注册机制:关注下线/摘除的

    2024年02月01日
    浏览(27)
  • ES 一些简单 的查询注意事项

    term query 不分词字段 带分数 where name=xxx filter 分词字段 不分词字段 不带分数 Terms query 所有类型 带分数 where name in(xxx) Range query where name between xxx and xxx Exists Regexp Match query 分词字段/基础字段 Multi-match query 多个分词字段/基础字段 Boolean query 复合查询 must should 带分数 filter must

    2024年02月12日
    浏览(87)
  • [C#][]操作注册表一些注意点

    C#注册表只需要引入 using Microsoft.Win32; C#注册表操作都是通过2个类Registry和RegistryKey进行所有操作。但是有些基本注意事项经常忘记,不常用就很容易忘记。 第一,打开注册表,第2个bool参数问题: RegistryKey key = Registry.LocalMachine; RegistryKey software = key.OpenSubKey(\\\"software\\\\MyAp

    2024年02月10日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包