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

news2024/12/23 14:42:48

大家好,又见面了。

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

不少小伙伴在评论中提出了一些的疑问或自己的独到见解,也在评论区中进行了热烈的互动讨论。梳理了下相关评论内容,针对此前文章中没有提及的一些典型讨论点拿出来聊一聊,也是作为对此前两篇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次的吧~

img

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这个词的含义解释:

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

img

字符串拼接明明有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是否还有哪些疑问或者自己的独特理解呢?欢迎一起交流下。

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

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

期待与你一起探讨,一起成长为更好的自己。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1006131.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端开发工程师:职业前景、工资、 具体工作

该篇适用于从零基础学习前端的小白 一、职业前景 前端这两年也是非常火的&#xff0c;就业的前景也是非常不错的。 1.需求持续增长&#xff1a; 随着互联网和移动设备的普及&#xff0c;越来越多的企业和组织需要建立和维护网站、应用程序和在线平台。这导致了对具有前端开发…

Spring Cloud Eureka:服务注册与发现

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; Spring Cloud Eureka&#xff1a;服务注册与发现 Spring Cloud Eureka是Spring Cloud生态系统中的一个组件&#xff0c;它是用于实现服务注册与发现的服务治理组件。在…

VMware16安装ghost版win7

文章目录 准备工作GHO 文件装机工具 新建虚拟机配置虚拟机还需要一个 CD/DVD PE 安装步骤分区还原挂载 CD/DVD开始还原 还原之后 准备工作 GHO 文件 可以去百度搜索这种文件&#xff0c;我这里是从系统之家下载的deepin win7 ghost 系统 装机工具 因为下载的 ghost 版的 w…

查找:分块查找算法分析

数据分块存储,分块查找特点:块内无序、块间有序。 1.分块查找的算法思想 1.使用顺序查找查索引 设置一个索引表&#xff0c; 索引表数据结构设计&#xff1a; //索引表 typedef struct {ElemType maxValue;int low,high; }Index;//顺序表存储实际元素 ElemType List[100] ;分…

圆的反演 hdu 4773

欢迎关注更多精彩 关注我&#xff0c;学习常用算法与数据结构&#xff0c;一题多解&#xff0c;降维打击。 题目大意 http://acm.hdu.edu.cn/showproblem.php?pid4773 给定2个不相交的圆以及圆外1点P。求过P并且与另两个圆相切&#xff08;外切&#xff09;的圆&#xff0c…

使用TortoiseGit导出两次提交时间之间的差异文件

同时选择两个提交时间&#xff0c;右键后点击Compare revisions 多选需要导出的待发布的文件&#xff0c;然后右键点击Export selection to... 在弹窗中选择文件夹&#xff08;导出待发布的文件&#xff09; 导出效果&#xff08;目录&#xff09; 导出效果&#xff08;文件&am…

MGR新节点RECOVERING状态的分析与解决:caching_sha2_password验证插件的影响

起因 在GreatSQL社区上有一位用户提出了“手工构建MGR碰到的次节点一直处于recovering状态”&#xff0c;经过排查后&#xff0c;发现了是因为新密码验证插件caching_sha2_password导致的从节点一直无法连接主节点&#xff0c;帖子地址&#xff1a;(https://greatsql.cn/threa…

基于Android系统图书管理系统

摘要 随着移动终端使用率的快速增加&#xff0c;Android智能产品已日益成为越来越多的人们选择的移动终端产品。伴随着Android智能手机与平板电脑已经在我们生活大量的使用&#xff0c;越来越多的基于Android开发平台的应用也随之产生。 便捷的图书检索和借阅&#xff1a;用户可…

Java“牵手”京东商品列表页数据采集+商品价格数据排序,商品销量排序数据,京东商品API采集方法

京东商品列表API是京东平台提供给开发者的应用程序编程接口&#xff0c;通过API可以获取京东平台上商品列表数据。 京东商品列表API可以提供多种不同的推荐商品列表API接口&#xff0c;开发者可以根据自己的需求选择适合自己的接口。其中&#xff0c;最常用的是基于用户反馈的…

项目经理如何做好跨部门的沟通与协作?

对项目经理来说沟通不良&#xff0c;会对项目造成严重影响&#xff0c;跨部门沟通更是项目管理中的难题。原本应该合作解决的问题&#xff0c;到了跨部门会议上&#xff0c;又各说各话&#xff0c;找不到共识。 在不同部门各有不同立场与利益的情况下&#xff0c;怎样才能把话…

Open Interpreter:OpenAI Code Interpreter的开源实现|本地化|可联网

如果你对这篇文章感兴趣&#xff0c;而且你想要了解更多关于AI领域的实战技巧&#xff0c;可以关注「技术狂潮AI」公众号。在这里&#xff0c;你可以看到最新最热的AIGC领域的干货文章和案例实战教程。 一、前言 今年7月&#xff0c;OpenAI发布了一个强大的插件&#xff0c;名…

什么是第三方软件测试?

测试机构 什么是软件测试&#xff1f; 软件测试是一个验证和验证应用程序功能以确定它是否满足要求的过程。这是在应用程序中发现缺陷并根据*终用户的要求检查应用程序功能的过程。 第三方软件检测机构是专门提供软件测试服务&#xff0c;其出具软件测试报告过程中可能运用到…

Object.keys和Object.values

Object.keys list:[],obj:{数据泄露: 5412, 数据传输: 3921, 数据篡改: 851392, 数据滥用: 59532 },//返回可枚举的属性数组console.log(Object.keys(this.obj)) // [数据泄露, 数据传输, 数据篡改, 数据滥用]Object.keys(this.obj).map(key>{this.list.push({title:key,val…

Java工作流系统,快速实现业务审批(源码)

前言 activiti工作流引擎项目&#xff0c;企业erp、oa、hr、crm等企事业办公系统轻松落地&#xff0c;请假审批demo从流程绘制到审批结束实例。 一、项目形式 springbootvueactiviti集成了activiti在线编辑器&#xff0c;流行的前后端分离部署开发模式&#xff0c;快速开发平…

700亿参数Llama 2训练加速195%!数据成为其提升效果的关键要素

Llama 2是Meta AI正式发布的最新一代开源大模型&#xff0c;达到了2万亿的token。精调Chat模型是在100万人类标注数据上训练。Llama 2在包括推理、编码、精通性和知识测试等许多外部基准测试中都优于其他开源语言模型。 Llama 2开启了全球范围内AI大型模型的共享新篇章。它包括…

第5章_freeRTOS入门与工程实践之模块使用说明与STM32CubeMX配置

本教程基于韦东山百问网出的 DShanMCU-F103开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id724601559592 配套资料获取&#xff1a;https://rtos.100ask.net/zh/freeRTOS/DShanMCU-F103 freeRTOS系列教程之freeRTOS入…

进销存仓库管理系统有哪些?哪些适合商户用?

进销存仓库管理系统可以帮助商家实现准确的库存控制、优化采购和销售活动&#xff0c;提升仓库操作效率&#xff0c;并提供数据分析和决策支持&#xff0c;从而解决企业在库存管理和供应链方面的问题&#xff0c;提升整体运营效率和竞争力。 进销存仓库管理系统有哪些&#xf…

SmartSQL 一款开源的数据库文档管理工具

建议直接蓝奏云下载安装 蓝奏云下载&#xff1a;https://wwoc.lanzoum.com/b04dpvcxe 蓝奏云密码&#xff1a;123 项目介绍 SmartSQL 是一款方便、快捷的数据库文档查询、导出工具&#xff01;从最初仅支持 数据库、CHM文档格式开始&#xff0c;通过不断地探索开发、集思广…

LVGL(72)-v8--滑块slider

一、slider 简介 1.1 概述 Overview Slider对象看起来像一个带有旋钮的工具条。可以拖动该旋钮来设置一个值。滑块也可以是垂直的或水平的。滑动条在前面我们介绍img控件的时候有个历程有使用到&#xff0c;哪里我们讲述设置样式实现对滑动条的一些样式的设置。 1.2 部分和风…

如果是你,会不会修改这段代码?

最近在合流一些功能&#xff0c;然后有一部分功能的代码需要重构。 需要重构的代码如下&#xff0c;没有重构之前是可以正确执行的。 unsigned int Hex2Int (char c) {if (c > 0 && c < 9) {return (unsigned int) (c - 0);} else {return (unsigned int)(c - a1…