⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 小姐姐味道 「小姐姐味道 (xjjdog)」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

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

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

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

public List<FeedItemVo> getFeeds(Query query,Page page){
List<String> orgiList = new ArrayList<>();

List<FeedItemVo> 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<String> 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()));

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

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

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

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

2. 舍得拆分函数

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

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

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

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

public Stream<OrderDto> 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<OrderDto> 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<OrderDto> getOrderByUser(String userId){
return orderRepo.findOrderByUser().stream()
.map(OrderDto::new);
}

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

Predicate<Registar> registarIsCorrect = reg -> 
reg.getRegulationId() != null
&& reg.getRegulationId() != 0
&& reg.getType() == 0;

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

3. 合理的使用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<String> 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人,需要多下一点功夫。

4. 返回Stream还是返回List?

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

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

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

public Stream<User> getAuthUsers(){    ...    return Stream.of(users);}

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

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

5. 少用或者不用并行流

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.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.3</version>
</dependency>

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

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

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

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

文章目录
  1. 1. 1. 合理的换行
  2. 2. 2. 舍得拆分函数
  3. 3. 3. 合理的使用Optional
  4. 4. 4. 返回Stream还是返回List?
  5. 5. 5. 少用或者不用并行流
  6. 6. 总结