合理使用Stream和Lambda

        Java8的stream流,加上lambda表达式,可以让代码变短变美,已经得到了广大开发者的应用,让我们再写一些复杂代码的时候,也有了更多的选择。
        代码主要是给人看的,其次才是给机器运行的。代码写的是否简洁明了,是否写的漂亮,对我们后续的bug修复和功能拓展,意义非凡。很多时候,能否写出优秀的代码,是和工具没有直接关系的。代码是工程师能力和修养的体现,有的人,即使使用了stream和lambda,代码依然像屎山。

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

public List<FolderItemVo> getFolders(Query query, Page page) {
	List<String> orgList = new ArrayList<>();
	List<FolderItemVo> collect = Page.getRecords().stream()
			.filter(this::addDetail)
			.map(FolderItemVo::converVo)
			.filter(vo -> this.addOrgNames(query.getIsShow(), orgList, vo))
			.collect(Collectors.toList());
	// ...其他代码逻辑
	return collect;
}

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

private boolean addOrgNames(boolean isShow, List<String> orgList, FolderItemVo vo) {
	if (isShow && vo.getOrgIds() != null) {
		orgList.add(vo.getOrgName());
	}
	return true;
}

如果觉得上面的不够过瘾的话,我们继续看下面这段

if (!CollectionUtils.isEmpty(roleNameStrList) && roleNameStrList.contains("test")) {
	vos = vos.stream().filter(
					vo -> !CollectionUtils.isEmpty(vo.getSpecialTaskIteamVoList())
							&& 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.getSpecialTaskIteamVoList())
							&& vo.getTaskName() != null)
			.collect(Collectors.toList());
}
result.addAll(vos.stream().collect(Collectors.toList()));

        表面看着代码能跑,但是感觉有点画蛇添足了。该缩进的不缩进,该换行的不换行,怎么看都算不上好的代码。

        那么需要如何去改善呢?除了技术问题,还有比较重要的就是意识问题。时刻需要记得,优秀的代码必须是可读性比较好的,其次才是功能完善。

 

1.合理的换行


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

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

Stream.of("a","b","c").map(toUpperCase()).map(toBase64()).collect(joining(" "));

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

Stream.of("a", "b", "c")
	.map(toUpperCase())
	.map(toBase64())
	.collect(joining(" "));

        不要觉得这种改造没有意义,或者觉得这样的换行是理所当然的。在平时的代码review中,像这样糅杂在一块的代码,真的是数不胜数,你完全搞不懂写代码的人的真正意图。所以合理的换行是让代码青春永驻的良方。

 

2.舍得拆分函数


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

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

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

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

return mapList.stream().map(data->{
	ExcelDataDTO dto = new ExcelDataDTO();
	dto.setSerialNumber(1);
	dto.setServiceName("货物或应税劳务、服务名称");
	dto.setUnitOfMeasurement("计量单位");
	dto.setSpecificationAndModel("规格型号");
	return dto;
});

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

return mapList.stream()
		.map(this::getExcelDataDTO);

public ExcelDataDTO getExcelDataDTO() {
	ExcelDataDTO dto = new ExcelDataDTO();
	dto.setSerialNumber(1);
	dto.setServiceName("货物或应税劳务、服务名称");
	dto.setUnitOfMeasurement("计量单位");
	dto.setSpecificationAndModel("规格型号");
	return dto;
};

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

return mapList.stream()
		.map(getExcelDataDTO::new);

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

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

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

 

3.合理的使用 Optional


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

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 = "abc";
String defaultEmail = userName.get() == null ? "" : userName.get() + "@abc.cn";

        应该修改成这样的方式:

Optional<String> userName = "abc";
String defaultEmail = userName
	.map(e -> e + "@abc.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 list) {
	List dst = new ArrayList<>();
	if (CollectionUtils.isEmpty(list)) {
		return dst;
	}
	list.stream()
		.parallel()
		.map(..)
		.filter(..)
		.foreach(dst::add);
	return dst;
}

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

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

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

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

        那已经知道弊端了怎么办呢?我们的做法就是一刀切,直接禁止。虽然残忍了些,但是也避免了很多不必要的问题出现。

 

6.总结


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

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

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

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

<dependency>
	<groupId>io.vavr</groupId>
	<artifactId>vavr</artifactId>
	<version>0.10.4</version>
</dependency>

        无论如何强大的 api 和编程方式,还是扛不住小伙伴的滥用。这些代码在逻辑上完全是说得通的,但是看起来别扭,维护起来费劲。写一堆垃圾 Lambda 代码是虐待同事的最好方式,也是埋雷的不二选择!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏目不听话

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值