- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
大家好,又见面了.
在此前我的文章中,曾分2篇详细探讨了下JAVA中Stream流的相关操作,2篇文章收获了累计 10w+ 阅读、 2k+ 点赞以及 5k+ 收藏的记录。能够得到众多小伙伴的认可,是技术分享过程中最开心的事情.
不少小伙伴在评论中提出了一些的疑问或自己的独到见解,也在评论区中进行了热烈的互动讨论。梳理了下相关评论内容,针对此前文章中没有提及的一些典型讨论点拿出来聊一聊,也是作为对此前两篇Java 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次的吧~ 。
自 Java8 引入了 Lambda 、 函数式接口 、 Stream 等新鲜内容以来,针对使用Stream或Lambda语法究竟是让代码更易懂还是更复杂的争议,一直就没有停止过。有的同学会觉得Stream语法的方式,一眼就可以看出业务逻辑本身的含义,也有一些同学认为使用了Stream之后代码的可读性降低了很多.
其实,这是个人编码模式与理念上的不同感知而已。Stream主打的就是让代码更聚焦自身逻辑,省去其余繁文缛节对代码逻辑的干扰,整体编码上会更加的简洁。但是刚接触的时候,难免会需要一定的适应期。技术总是在不断迭代、不断拥抱新技术、不去刻意排斥新技术,或许是一个更好的选项.
那么,话说回来,如何让自己能够一眼看懂Stream代码、感受到Stream的简洁之美呢?分享个人的一个经验:
下面举了个例子,如何用上述的2条方法,快速的让自己理解一段Stream代码表达的意思.
那么上面这段代码的含义就是,先根据员工子公司过滤所有上海公司的人员,再获取员工工资最高的那个人信息。怎么样?按照这个方法,是不是可以发现,Stream的方式,确实更加容易理解了呢~ 。
技术分享其实是一个双向的过程,分享的同时,也是自我学习与提升的机会,除了可以梳理发现一些自己之前忽略的知识点并加以巩固,还可以在互动的时候get到新的技能.
比如,我在此前的 Java Stream 介绍的文章中,有提过基于Stream进行编码的时候会导致代码 debug调试 的时候会比较困难,尤其是那种只有一行Lambda表达式的情况(因为如果代码逻辑多行编写的时候,可以在代码块内部打断点,这样其实也可以进行debug调试).
关于这一点,很多小伙伴也有相同的感受,比如下面这个评论:
你以为这就结束了?接下来一个小伙伴的提示,“震惊”了众人!纳尼?原来Stream代码段也是可以debug单步调试的?
跟踪Stream中单步处理过程的操作入口按钮长这样:
并且,另一个小伙伴补充说这是 IDEA 从 2019.03 版本开始有的功能:
嗯?难怪呢,我一直用的2019.02版本的,所以才没用上这个功能(强行给自己找了个台阶、哈哈哈)。于是,我悄悄的将自己的idea升级到了最新的2023.02版本(PS:新版本的UI挺好看,就是bug贼多)。好啦,言归正传,那么究竟应该如何利用IDEA来实现单步DEBUG呢?一一起来感受下吧.
在代码行前面添加断点的时候,如果要打断点的这行代码里面包含Stream 中间方法 ( map\filter\sort 之类的)的时候,会提示让选择 断点的具体类型 .
一共有 三种 类型断点可供选择:
下面这个图可以更清晰的解释清楚上述三者的区别。一般来说,我们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不好调试吗?至少我不会了.
在我们常规的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 和 foreach 在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文档中的描述:
因为对于 findFirst 、 count 之类的方法,peek操作被视为 与结果无关联的操作 ,直接被优化掉不执行了。所以说最好按照API设计时预期的场景去使用API,避免自己给自己埋坑.
我们从peek的源码的注释上可以看出,peek的推荐使用场景是用于一些调试场景,可以借助peek来将各个元素的信息打印出来,便于开发过程中的调试与问题定位分析.
我们再看下peek这个词的含义解释:
既然开发者给它起了这么个名字,似乎确实仅是为了窥视执行过程中数据的变化情况。为了避免让自己踩坑,最好按照设计者推荐的用途用法进行使用,否则即使现在没问题,也不能保证后续版本中不会出问题.
在介绍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 是否还有哪些疑问或者自己的独特理解呢?欢迎一起交流下.
传送门:
我是悟道,聊技术、又不仅仅聊技术~ 。
如果觉得有用,请 点赞 + 关注 让我感受到您的支持。也可以关注下我的公众号【架构悟道】,获取更及时的更新.
期待与你一起探讨,一起成长为更好的自己.
最后此篇关于再聊JavaStream的一些实战技能与注意点的文章就讲到这里了,如果你想了解更多关于再聊JavaStream的一些实战技能与注意点的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在运行PHP脚本,并继续收到如下错误: 注意:未定义的变量:第10行的C:\ wamp \ www \ mypath \ index.php中的my_variable_name 注意
我正在运行PHP脚本,并继续收到如下错误: 注意:未定义的变量:第10行的C:\ wamp \ www \ mypath \ index.php中的my_variable_name 注意
我正在运行PHP脚本,并继续收到如下错误: 注意:未定义的变量:第10行的C:\ wamp \ www \ mypath \ index.php中的my_variable_name 注意
我正在运行一个PHP脚本,并且继续收到如下错误:。第10行和第11行如下所示:。这些错误消息的含义是什么?。为什么他们突然出现了?我多年来一直使用这个脚本,从来没有遇到过任何问题。。我该怎么修理它们呢
当我在 flutter clean 之后运行 flutter run 或 debug my code 时显示此错误 Note: C:\src\flutter.pub-cache\hosted\pub.
My Goal: To fix this error and be able to run my app without an error. Error Message: Note:D:\Learni
前言:今天在解决一个问题时,程序总是不能输出正确值,分析逻辑思路没问题后,发现原来是由于函数传递导致了这个情况。 LeetCode 113 问题:给你二叉树的根节点
我正在 R 中开发一个包,当我运行时 devtools::check()我收到以下说明。 checking DESCRIPTION meta-information ... NOTE Malforme
获得通知和警告波纹管 Notice: Use of undefined constant GLOB_BRACE - assumed 'GLOB_BRACE' in /var/www/html/open
我正在准备一个 R 包以提交给 CRAN。 R CMD 检查给了我以下注意: Foreign function calls to a different package: .Fortran("cinc
我正在尝试从以下位置获取数据: http://api.convoytrucking.net/api.php?api_key=public&show=player&player_name=Mick_Gi
我有这段代码,但我不明白为什么我仍然有这个错误,我已经尝试了所有解决方案,但无法解决这个问题:-注意:未定义索引:product_price-注意:未定义索引:product_quantity-注意:
This question already has answers here: “Notice: Undefined variable”, “Notice: Undefined index”, and
我正在尝试从以下位置获取数据: http://api.convoytrucking.net/api.php?api_key=public&show=player&player_name=Mick_Gi
切记,在PHP 7中不要做的10件事 1. 不要使用 mysql_ 函数 这一天终于来了,从此你不仅仅“不应该”使用mysql_函数。PHP 7 已经把它们从核心中全部移除了,也就是说你需要迁移
前几天安装了dedecms系统,当在后台安全退出的时候,后台出现空白,先前只分析其他功能去了,也没太注意安全,看了一下安全退出的代码,是这样写的: 复制代码 代码如下: function ex
我使用此代码来检查变量$n0、$n1、$n2是否未定义。 但每次未定义时我都会收到通知。我的代码是一种不好的做法吗?还有什么替代方案吗?或者只是删除通知,代码就可以了? if
编写代码时处理所有警告是否重要?在我公司中具有较高资历的开发人员坚持认为警告是无害的。诚然,其中一些是: Warning: Division by zero Notice: Undefined ind
我有一个搜索查询,执行搜索查询后,我将$ result放入数组中。 我的PHP代码- $contents = $client->search($params); // executing the se
This question already has answers here: “Notice: Undefined variable”, “Notice: Undefined index”, and
我是一名优秀的程序员,十分优秀!