引言
伴随着Java8的普及(有些小伙伴可能已经在玩Java17了,当然这不是重点),它为我引入了流式编程,由于本人在早些时候接触过Scala的函数式编程,所以非常喜欢一路点点的操作。
这不今天的Bug就这样产生了, 背景是因为本人在编程中多少有些代码洁癖,而且公司现在引入了一套代码质量评测平台,这样就不得不对已有的代码进行重构。经过本人一段时间的奋斗终于将2k+的code smell 降到了100以内, 项目的代码质量也在所属团队处于前列,一度使得我还得意了一阵子。好了闲话不说了, 接下来我们直接进入正题好了。
Bug 引入
在代码优化中, 我将已有的生成Map的操作改成了一下的样子。
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
public class CollectorsToMapTest {
@Test
void test_toMap() {
// 模拟制造重复数据
List<Employee> employees = Stream.of(Employee.create("zhangsan", 18), Employee.create("zhangsan", 18))
.collect(Collectors.toList());
Map<String, Employee> employeeMap = employees.stream().collect(Collectors.toMap(Employee::getName, Function.identity()));
System.out.println(employeeMap);
}
private static class Employee {
private String name;
private int age;
private Employee(String name, int age) {
this.name = name;
this.age = age;
}
private static Employee create(String name, int age) {
return new Employee(name, age);
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Employee [name=" + name + ", age=" + age + "]";
}
}
}
执行如上代码,抛出异常
这个跟我们日常理解的Map存放键值对的操作相悖, 到底是怎么回事呢?
Bug 原因分析
跟入Collectors.toMap 内部查看源码, 俗话说万事不决,撸源码就是这个道理哈。
答案已经找到,源码诚不欺我。 toMap 方法默认给了一个不合并重复key的mergeOperation, 那这个mergeOperation 在哪用的呢?继续追打源码。
接着跟入m1.merge(), 选择HashMap
好了,HashMap 的调用mergeOperation的点已经找到了。
总结一下,Collector.toMap(keyGen, valGen) 默认不merge重复key的entry, 一旦遇到重复的item 会抛出IllegalStatusException Dulipcate key xxx, 特别注意这里的 xxx 是重复key是之前的value而不是重复的key, 这一句日志打的也很有迷惑性,如果不跟进源码很难理解其真正含义(跟领导解释好久,他不理解,就带他看源码了哦 --- Talk is cheap, show me the code)。
Bug Fix
既然知道根因了, 我们应该怎么去解决这个问题呢?
其实Collectors.toMap的重载方法已经给出了答案
将之前的Collector.toMap 改成如下的样子就OK了
Collectors.toMap(Employee::getName, Function.identity(), (prev, cur) -> cur)
(prev, cur) -> cur ---- 这是个匿名函数,指出了merge操作该怎么进行。这里参考HashMap实现重复时用新值, 当然你可以像下面这样玩。
(prev, cur) -> prev ---- 不更新
(prev, cur) -> prev+cur --- 累加value
...
具体就要业务逻辑而定了。
写在最后
既然不带mergeOperation的toMap操作会引起bug, 为什么还要提供呢?
我理解, 牛刀杀牛,鸡刀杀鸡了。 避免资源浪费,也避免到处都提供mergeFunction导致代码可读性不高。
总结下
> 对于key值不会重复的场景,用Collectors.toMap(keyGen, valGen)就可以了
> 对于key值可能会重复的场景,上Collectors.toMap(keyGen, valGen, mergeOperation)避免bug
key值不重复的场景一般有一下几种:
DB sequence 前置service已经去重,如查询接口 用户输入信息,保证去重时 ....