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

Java后端技术

共 7746字,需浏览 16分钟

 · 2022-04-28

往期热门文章:

1、一不小心节约了 591 台机器!
2、你见过哪些目瞪口呆的 Java 代码技巧?
3、笑死!程序员延寿指南开源了
4、互联网黑话,被上海人翻译火了
5、还在用分页?太Low !试试 MyBatis 流式查询,真心爽!

文章来源:【公众号:小姐姐味道】

目录
  • 合理的换行

  • 舍得拆分函数

  • 合理的使用 Optional

  • 返回 Stream 还是返回 List?

  • 少用或者不用并行流

  • 总结


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

代码首先是给人看的,其次才是给机器执行的。代码写的是否简洁明了,是否写的漂亮,对后续的 Bug 修复和功能扩展,意义重大。

很多时候,是否能写出优秀的代码,是和工具没有关系的。代码是工程师能力和修养的体现,有的人,即使用了 Stream,用了 Lambda,代码也依然写的像屎一样。

不信,我们来参观一下一段美妙的代码。好家伙,filter 里面竟然带着潇洒的逻辑。
public List getFeeds(Query query,Page page){
    List orgiList = new ArrayList<>();

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

private boolean addDetail(FeedItem feed){
    vo.setItemCardConf(service.getById(feed.getId()));
    return true;
}

private boolean addOrgNames(boolean isSlow,List orgiList,FeedItemVo vo){
    if(isShow && vo.getOrgIds() != null){
        orgiList.add(vo.getOrgiName());
    }
    return true;
}

如果觉得不过瘾的话,我们再贴上一小段。
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()));

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

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

合理的换行


在 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 中,这种糅杂在一块的代码,真的是数不胜数,你完全搞不懂写代码的人的意图。

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

舍得拆分函数


为什么函数能够越写越长?是因为技术水平高,能够驾驭这种变化么?答案是因为懒!

由于开发工期或者意识的问题,遇到有新的需求,直接往老的代码上添加 if else,即使遇到相似的功能,也直接选择将原来的代码拷贝过去。久而久之,码将不码。

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

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

我将拿一个经常在代码中出现的实体转换示例来说明一下。下面的转换,创建了一个匿名的函数 order->{},它在语义表达上,是非常弱的。
public Stream getOrderByUser(String userId){
    return orderRepo.findOrderByUser().stream()
        .map(order-> {
            OrderDto dto = new OrderDto();
            dto.setOrderId(order.getOrderId());
            dto.setTitle(order.getTitle().split("#")[0]);
            dto.setCreateDate(order.getCreateDate().getTime());
            return dto;
    });
}

在实际的业务代码中,这样的赋值拷贝还有转换逻辑通常非常的长,我们可以尝试把 dto 的创建过程给独立开来。

因为转换动作不是主要的业务逻辑,我们通常不会关心其中到底发生了啥。
public Stream getOrderByUser(String userId){
    return orderRepo.findOrderByUser().stream()
        .map(this::toOrderDto);
}
public OrderDto toOrderDto(Order order){
    OrderDto dto = new OrderDto();
            dto.setOrderId(order.getOrderId());
            dto.setTitle(order.getTitle().split("#")[0]);
            dto.setCreateDate(order.getCreateDate().getTime());
    return dto;
}

这样的转换代码还是有点丑。但如果 OrderDto 的构造函数,参数就是 Order 的话 public OrderDto(Order order),那我们就可以把真个转换逻辑从主逻辑中移除出去,整个代码就可以非常的清爽。
public Stream getOrderByUser(String userId){
    return orderRepo.findOrderByUser().stream()
        .map(OrderDto::new);
}

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

比如:
Predicate registarIsCorrect = reg -> 
    reg.getRegulationId() != null 
    && reg.getRegulationId() != 0 
    && reg.getType() == 0;

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

合理的使用 Optional


在 Java 代码里,由于 NullPointerException 不属于强制捕捉的异常,它会隐藏在代码里,造成很多不可预料的 bug。

所以,我们会在拿到一个参数的时候,都会验证它的合法性,看一下它到底是不是 null,代码中到处充满了这样的代码。
if(null == obj)
if(null == user.getName() || "".equals(user.getName()))

if (order != null) {
    Logistics logistics = order.getLogistics();
    if(logistics != null){
        Address address = logistics.getAddress();
        if (address != null) {
            Country country = address.getCountry();
            if (country != null) {
                Isocode isocode = country.getIsocode();
                if (isocode != null) {
                    return isocode.getNumber();
                }
            }
        }
    }
}

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

上面比较复杂的代码示例,就可以替换成下面的代码:
 String result = 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 消灭在萌芽中。
public Optional getUserName({
    return Optional.ofNullable(userName);
}

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

比如:
Optional<String> userName = "xjjdog";
String defaultEmail = userName.get() == null ? "":userName.get() + "@xjjdog.cn";

而应该修改成这样的方式:
Optional<String> userName = "xjjdog";
String defaultEmail = userName
    .map(e -> e + "@xjjdog.cn")
    .orElse("");

那为什么我们的代码中,依然充满了各式各样的空值判断?即使在非常专业和流行的代码中?

一个非常重要的原因,就是 Optional 的使用需要保持一致。当其中的一环出现了断层,大多数编码者都会以模仿的方式去写一些代码,以便保持与原代码风格的一致。

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

返回 Stream 还是返回 List?


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

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

但对于一个 Stream 来说,是不可变的,它不会影响原始的集合。对于这种场景,我们推荐直接返回 Stream 流,而不是返回集合。

这种方式还有一个好处,能够强烈的暗示 API 使用者,多多使用 Stream 相关的函数,以便能够统一代码风格。
public Stream getAuthUsers(){
    ...
    return Stream.of(users);
}

不可变集合是一个强需求,它能防止外部的函数对这些集合进行不可预料的修改。在 guava 中,就有大量的 Immutable 类支持这种包裹。

再举一个例子,Java 的枚举,它的 values() 方法,为了防止外面的 api 对枚举进行修改,就只能拷贝一份数据。

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

少用或者不用并行流


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

并行流一个老生常谈的问题,就是线程安全问题。在迭代的过程中,如果使用了线程不安全的类,那么就容易出现问题。

比如下面这段代码,大多数情况下运行都是错误的:
List transform(List source){
 List dst = new ArrayList<>();
 if(CollectionUtils.isEmpty()){
  return dst;
 }
 source.stream.
  .parallel()
  .map(..)
  .filter(..)
  .foreach(dst::add);
 return dst;
}

你可能会说,我把 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 代码,是虐待同事最好的方式,也是埋坑的不二选择。

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

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

往期热门文章:

1、笑死!程序员延寿指南开源了
2、用 Dubbo 传输文件?被老板一顿揍!
3、45 个 Git 经典操作场景,专治不会合代码!
4、@Transactional 注解失效的3种原因及解决办法
5、小学生们在B站讲算法,网友:我只会阿巴阿巴
6、Spring爆出比Log4j2还大的漏洞?
7、Objects.equals 有坑!!!
8、Redis 官方可视化工具,功能真心强大!
8、xxl-job 和 ElasticJob比,谁牛?
10、推荐好用的 Spring Boot 内置工具类

浏览 5
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报