0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

用了Stream后,代码反而越写越丑?

jf_ro2CN3Fa 来源:芋道源码 2023-08-23 15:08 次阅读

Java8的stream流,加上lambda表达式,可以让代码变短变美,已经得到了广泛的应用。我们在写一些复杂代码的时候,也有了更多的选择。

代码首先是给人看的,其次才是给机器执行的。代码写的是否简洁明了,是否写的漂亮,对后续的bug修复和功能扩展,意义重大。很多时候,是否能写出优秀的代码,是和工具没有关系的。代码是工程师能力和修养的体现,有的人,即使用了stream,用了lambda,代码也依然写的像屎一样。

不信,我们来参观一下一段美妙的代码。好家伙,filter里面竟然带着潇洒的逻辑。

publicListgetFeeds(Queryquery,Pagepage){
ListorgiList=newArrayList<>();

Listcollect=page.getRecords().stream()
.filter(this::addDetail)
.map(FeedItemVo::convertVo)
.filter(vo->this.addOrgNames(query.getIsSlow(),orgiList,vo))
.collect(Collectors.toList());
//...其他逻辑
returncollect;
}

privatebooleanaddDetail(FeedItemfeed){
vo.setItemCardConf(service.getById(feed.getId()));
returntrue;
}

privatebooleanaddOrgNames(booleanisSlow,ListorgiList,FeedItemVovo){
if(isShow&&vo.getOrgIds()!=null){
orgiList.add(vo.getOrgiName());
}
returntrue;
}

如果觉得不过瘾的话,我们再贴上一小段。

if(!CollectionUtils.isEmpty(roleNameStrList)&&roleNameStrList.contains(REGULATORY_ROLE)){
vos=vos.stream().filter(
vo->!CollectionUtils.isEmpty(vo.getSpecialTaskItemVoList())
&&vo.getTaskName()!=null)
.collect(Collectors.toList());
}else{
vos=vos.stream().filter(vo->vo.getIsSelect()
&&vo.getTaskName()!=null)
.collect(Collectors.toList());
vos=vos.stream().filter(
vo->!CollectionUtils.isEmpty(vo.getSpecialTaskItemVoList())
&&vo.getTaskName()!=null)
.collect(Collectors.toList());
}
result.addAll(vos.stream().collect(Collectors.toList()));

代码能跑,但多画蛇添足。该缩进的不缩进,该换行的不换行,说什么也算不上好代码。

如何改善?除了技术问题,还是一个意识问题。时刻记得,优秀的代码,首先是可读的,然后才是功能完善。

1. 合理的换行

在Java中,同样的功能,代码行数写的少了,并不见得你的代码就好。由于Java使用;作为代码行的分割,如果你喜欢的话,甚至可以将整个Java文件搞成一行,就像是混淆后的JavaScript一样。

当然,我们知道这么做是不对的。在lambda的书写上,有一些套路可以让代码更加规整。

Stream.of("i","am","xjjdog").map(toUpperCase()).map(toBase64()).collect(joining(""));

上面这种代码的写法,就非常的不推荐。除了在阅读上容易造成障碍,在代码发生问题的时候,比如抛出异常,在异常堆栈中找问题也会变的困难。所以,我们要将它优雅的换行。

Stream.of("i","am","xjjdog")
.map(toUpperCase())
.map(toBase64())
.collect(joining(""));

不要认为这种改造很没有意义,或者认为这样的换行是理所当然的。在我平常的代码review中,这种糅杂在一块的代码,真的是数不胜数,你完全搞不懂写代码的人的意图。

合理的换行是代码青春永驻的配方。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

2. 舍得拆分函数

为什么函数能够越写越长?是因为技术水平高,能够驾驭这种变化么?答案是因为懒!由于开发工期或者意识的问题,遇到有新的需求,直接往老的代码上添加ifelse,即使遇到相似的功能,也直接选择将原来的代码拷贝过去。久而久之,码将不码。

首先聊一点性能方面的。在JVM中,JIT编译器会对调用量大,逻辑简单的代码进行方法内联,以减少栈帧的开销,并能进行更多的优化。所以,短小精悍的函数,其实是对JVM友好的。

在可读性方面,将一大坨代码,拆分成有意义的函数,是非常有必要的,也是重构的精髓所在。在lambda表达式中,这种拆分更是有必要。

我将拿一个经常在代码中出现的实体转换示例来说明一下。下面的转换,创建了一个匿名的函数order->{},它在语义表达上,是非常弱的。

publicStreamgetOrderByUser(StringuserId){
returnorderRepo.findOrderByUser().stream()
.map(order->{
OrderDtodto=newOrderDto();
dto.setOrderId(order.getOrderId());
dto.setTitle(order.getTitle().split("#")[0]);
dto.setCreateDate(order.getCreateDate().getTime());
returndto;
});
}

在实际的业务代码中,这样的赋值拷贝还有转换逻辑通常非常的长,我们可以尝试把dto的创建过程给独立开来。因为转换动作不是主要的业务逻辑,我们通常不会关心其中到底发生了啥。

publicStreamgetOrderByUser(StringuserId){
returnorderRepo.findOrderByUser().stream()
.map(this::toOrderDto);
}
publicOrderDtotoOrderDto(Orderorder){
OrderDtodto=newOrderDto();
dto.setOrderId(order.getOrderId());
dto.setTitle(order.getTitle().split("#")[0]);
dto.setCreateDate(order.getCreateDate().getTime());
returndto;
}

这样的转换代码还是有点丑。但如果OrderDto的构造函数,参数就是Order的话public OrderDto(Order order),那我们就可以把真个转换逻辑从主逻辑中移除出去,整个代码就可以非常的清爽。

publicStreamgetOrderByUser(StringuserId){
returnorderRepo.findOrderByUser().stream()
.map(OrderDto::new);
}

除了map和flatMap的函数可以做语义化,更多的filter可以使用Predicate去代替。比如:

PredicateregistarIsCorrect=reg->
reg.getRegulationId()!=null
&®.getRegulationId()!=0
&®.getType()==0;

registarIsCorrect,就可以当作filter的参数。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3. 合理的使用Optional

在Java代码里,由于NullPointerException不属于强制捕捉的异常,它会隐藏在代码里,造成很多不可预料的bug。所以,我们会在拿到一个参数的时候,都会验证它的合法性,看一下它到底是不是null,代码中到处充满了这样的代码。

if(null==obj)
if(null==user.getName()||"".equals(user.getName()))

if(order!=null){
Logisticslogistics=order.getLogistics();
if(logistics!=null){
Addressaddress=logistics.getAddress();
if(address!=null){
Countrycountry=address.getCountry();
if(country!=null){
Isocodeisocode=country.getIsocode();
if(isocode!=null){
returnisocode.getNumber();
}
}
}
}
}

Java8引入了Optional类,用于解决臭名昭著的空指针问题。实际上,它是一个包裹类,提供了几个方法可以去判断自身的空值问题。

上面比较复杂的代码示例,就可以替换成下面的代码。

Stringresult=Optional.ofNullable(order)
.flatMap(order->order.getLogistics())
.flatMap(logistics->logistics.getAddress())
.flatMap(address->address.getCountry())
.map(country->country.getIsocode())
.orElse(Isocode.CHINA.getNumber());

当你不确定你提供的东西,是不是为空的时候,一个好的习惯是不要返回null,否则调用者的代码将充满了null的判断。我们要把null消灭在萌芽中。

publicOptionalgetUserName(){
returnOptional.ofNullable(userName);
}

另外,我们要尽量的少使用Optional的get方法,它同样会让代码变丑。比如:

OptionaluserName="xjjdog";
StringdefaultEmail=userName.get()==null?"":userName.get()+"@xjjdog.cn";

而应该修改成这样的方式:

OptionaluserName="xjjdog";
StringdefaultEmail=userName.map(e->e+"@xjjdog.cn").orElse("");

那为什么我们的代码中,依然充满了各式各样的空值判断?即使在非常专业和流行的代码中?一个非常重要的原因,就是Optional的使用需要保持一致。当其中的一环出现了断层,大多数编码者都会以模仿的方式去写一些代码,以便保持与原代码风格的一致。

如果想要普及Optional在项目中的使用,脚手架设计者或者review人,需要多下一点功夫。

4. 返回Stream还是返回List?

很多人在设计接口的时候,会陷入两难的境地。我返回的数据,是直接返回Stream,还是返回List?

如果你返回的是一个List,比如ArrayList,那么你去修改这个List,会直接影响里面的值,除非你使用不可变的方式对其进行包裹。同样的,数组也有这样的问题。

但对于一个Stream来说,是不可变的,它不会影响原始的集合。对于这种场景,我们推荐直接返回Stream流,而不是返回集合。这种方式还有一个好处,能够强烈的暗示API使用者,多多使用Stream相关的函数,以便能够统一代码风格。

publicStreamgetAuthUsers(){...returnStream.of(users);}

不可变集合是一个强需求,它能防止外部的函数对这些集合进行不可预料的修改。在guava中,就有大量的Immutable类支持这种包裹。再举一个例子,Java的枚举,它的values()方法,为了防止外面的api对枚举进行修改,就只能拷贝一份数据。

但是,如果你的api,面向的是最终的用户,不需要再做修改,那么直接返回List就是比较好的,比如函数在Controller中。

5. 少用或者不用并行流

Java的并行流有很多问题,这些问题对并发编程不熟悉的人高频率踩坑。不是说并行流不好,但如果你发现你的团队,老在这上面栽跟头,那你也会毫不犹豫的降低推荐的频率。

并行流一个老生常谈的问题,就是线程安全问题。在迭代的过程中,如果使用了线程不安全的类,那么就容易出现问题。比如下面这段代码,大多数情况下运行都是错误的。

Listtransform(Listsource){Listdst=newArrayList<>();if(CollectionUtils.isEmpty()){returndst;}source.stream..parallel().map(..).filter(..).foreach(dst::add);returndst;}

你可能会说,我把foreach改成collect就行了。但是注意,很多开发人员是没有这样的意识的。既然api提供了这样的函数,它在逻辑上又讲得通,那你是阻挡不住别人这么用的。

并行流还有一个滥用问题,就是在迭代中执行了耗时非常长的IO任务。在用并行流之前,你有没有一个疑问?既然是并行,那它的线程池是怎么配置的?

很不幸,所有的并行流,共用了一个ForkJoinPool。它的大小,默认是CPU个数-1,大多数情况下,是不够用的。

如果有人在并行流上跑了耗时的IO业务,那么你即使执行一个简单的数学运算,也需要排队。关键是,你是没办法阻止项目内的其他同学使用并行流的,也无法知晓他干了什么事情。

那怎么办?我的做法是一刀切,直接禁止。虽然残忍了一些,但它避免了问题。

总结

Java8加入的Stream功能非常棒,我们不需要再羡慕其他语言,写起代码来也更加行云流水。虽然看着很厉害的样子,但它也只不过是一个语法糖而已,不要寄希望于用了它就获得了超能力。

随着Stream的流行,我们的代码里这样的代码也越来越多。但现在很多代码,使用了Stream和Lambda以后,代码反而越写越糟,又臭又长以至于不能阅读。没其他原因,滥用了!

总体来说,使用Stream和Lambda,要保证主流程的简单清晰,风格要统一,合理的换行,舍得加函数,正确的使用Optional等特性,而且不要在filter这样的函数里加代码逻辑。在写代码的时候,要有意识的遵循这些小tips,简洁优雅就是生产力。

如果觉得Java提供的特性还是不够,那我们还有一个开源的类库vavr,提供了更多的可能性,能够和Stream以及Lambda结合起来,来增强函数编程的体验。

<dependency>
<groupId>io.vavrgroupId>
<artifactId>vavrartifactId>
<version>0.10.3version>
dependency>

但无论提供了如何强大的api和编程方式,都扛不住小伙伴的滥用。这些代码,在逻辑上完全是说的通的,但就是看起来别扭,维护起来费劲。

写一堆垃圾lambda代码,是虐待同事最好的方式,也是埋坑的不二选择。

写代码嘛,就如同说话、聊天一样。大家干着同样的工作,有的人说话好听颜值又高,大家都喜欢和他聊天;有的人不好好说话,哪里痛戳哪里,虽然他存在着但大家都讨厌。

代码,除了工作的意义,不过是我们在世界上表达自己想法的另一种方式罢了。如何写好代码,不仅仅是个技术问题,更是一个意识问题。


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2904

    浏览量

    102994
  • 代码
    +关注

    关注

    30

    文章

    4555

    浏览量

    66769
  • Stream
    +关注

    关注

    0

    文章

    20

    浏览量

    7923

原文标题:用了Stream后,代码反而越写越丑?

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    峰PCB软件设计平台开发设计

    峰PCB软件设计平台开发设计replyview]青峰PCB软件设计平台.rar[/hide]
    发表于 12-18 11:04

    国产EDA软件---青

    锋PCB软件是目前国内唯一自主研发的EDA软件,整个软件由四部分组成:原理图编辑器(Schdoc),原理图库(Schlib),PCB编辑器(Pcbdoc),PCB元件封装库(PCblib).软件
    发表于 02-03 12:59

    锋能否取代Protel?

    ,PCB部分也符合用户的基本要求,高端功能,也处于后期的研发阶段。目前就功能而言,用户完全可以运用青锋进行PCB板的设计。不过考虑过往用户对Protel的依赖、用户的个人喜好,及部分用户对国产技术产品的偏见,青锋能否取代Protel的问题,就需要用户自己详细体验
    发表于 02-23 17:22

    [原创]青锋之99篇

      “开源”的姗姗来迟如雨后春笋般给中国大地添加了不少锦绣色彩,ERP,CAD的脱胎诞生在此延续生命的价值。然而就在此刻土生土长的国产PCB 软件—“青锋”破茧萌生了。如今在经历
    发表于 06-12 17:04

    请教:距离近报警声急促是什么原理

    各位高人。哪位知道倒车雷达或防丢器,距离近报警声急促是啥原理。有没有可以用WIFI标识卡实现这些功能的设备。谢谢
    发表于 04-12 06:47

    LCD上显示三角形圆形直线,错!!求大神指教

    点击直线显示的代码,他就提示其他没有更改的函数出现错误,然后就开始错了,求大神指教!!!过2天就交了 十分紧急 本人QQ372384899
    发表于 01-03 14:05

    社区之星:努力,幸运的24不可说

    ,平时论坛的活动也是积极参与,所以小编在此深深鞠躬,非常感谢24不可说对论坛的付出。帖子前,小编和24不可说交流了一下,小编问他最喜欢的一句话是什么,他说是“努力,幸运。”,所以希望他是——
    发表于 10-14 17:03

    推挽电路交失真问题求助

    如图是一个推挽电路,为什么会发生交失真呢,图上不是有偏置电压吗,如果我说错了,谁能解释下吗,谢谢
    发表于 04-11 17:46

    保险丝“小”吃香

    至今,保险丝已经朝着越来“小”的方向发展了,看来这年头还真的是“小”吃香这是为何呢?众所周知,现如今的电脑已经慢慢走向了微型时代,保险丝也不例外,随着经济科技的不断发展和进步,保险丝也变得
    发表于 05-31 10:11

    labview程序运行久越慢的现象

    一次循环显示画面,会停留一小段时间,约几秒。然后进入下一次循环。运行时间久,两次循环之间停留的时间会越来久。每次显示,数据都要进行处理。但并没有进行存储。所以应该没有出现数据不
    发表于 12-07 14:27

    程序员优秀吗?

    注重自己的身体,但仍然还不足够。留言:Frank Silbermann在Pertinent 文章留下的评论:速度快的程序员能通过他们高人一等的短期记忆来编出杂乱无章的代码、迅速的完成任务。这些程序员当
    发表于 10-25 10:04

    请问x1830人脸识别开发板,开机偶尔会停留在开机页面,显示logo不进入系统,而且使用半小时卡是什么原因?

    `请问x1830人脸识别开发板,开机偶尔会停留在开机页面,显示logo不进入系统,而且使用半小时卡是什么原因?`
    发表于 05-29 17:24

    PCB的线宽宽越好吗

    PCB的线宽宽越好吗?PCB线宽与电流有何关系?
    发表于 09-30 09:28

    AXI-Stream代码

    AXI-Stream代码详解 AXI4-Stream跟AXI4的区别在于AXI4-Stream没有ADDR接口,这样就不涉及读写数据的概念了,只有简单的发送与接收说法,减少了延时,允许
    的头像 发表于 11-05 17:40 2922次阅读
    AXI-<b class='flag-5'>Stream</b><b class='flag-5'>代码</b>

    在SpinalHDL里在顶层一键优化Stream/Flow代码生成

        在SpinalHDL里在顶层一键优化代码Stream/Flow代码生成的payload,fragment。 难看的代码       来看一段
    的头像 发表于 12-14 09:05 335次阅读