Java Stream 生产级代码编写
------ 从一次 Duplicate key 异常问题说起
Stream 很优雅,但它从来不是"安全的" 。
大多数 Stream 的坑,只有在业务数据复杂之后才会暴露。
一、事故背景:一个看似无害的 toMap
项目提测后,接口在运行过程中突然大量报错:
java.lang.IllegalStateException: Duplicate key
堆栈指向一行非常"正常"的代码:
java
Map<Long, UserDTO> userMap = users.stream()
.collect(Collectors.toMap(
UserDTO::getUserId,
Function.identity()
));
这行代码在开发环境 未出过问题,但在测试环境业务数据下直接导致接口 500。
二、问题根因:Stream 并不会替你做"业务判断"
1️⃣ Collectors.toMap() 的真实语义
java
toMap(keyMapper, valueMapper)
等价于:
-
你告诉 Java:
- key 怎么算
- value 是什么
-
但你没告诉它:
- 如果 key 重复怎么办
Java 的默认策略是:
❌ 我不猜,直接抛异常
2️⃣ 真正导致事故的原因
异常中的对象内容(简化):
UserDTO(
id = 620344009420726272,
userId = null
)
而代码使用的是:
java
UserDTO::getUserId
结果就是:
- 第一个 UserDTO → key = null
- 第二个 UserDTO → key = null
- Map 中 null key 冲突
toMap直接抛IllegalStateException
👉 这不是 Stream 的 bug,而是业务假设错误
三、Function.identity() 到底是干嘛的?
很多人会把这行当"魔法代码":
java
Function.identity()
其实它只是:
java
t -> t
也就是说:
value 就是当前 Stream 中的元素本身
在下面这段代码里:
java
Collectors.toMap(
UserDTO::getId,
Function.identity()
)
含义非常明确:
Map<Long, UserDTO>
四、真正的关键:mergeFunction
1️⃣ 什么是 mergeFunction?
java
toMap(keyMapper, valueMapper, mergeFunction)
第三个参数的含义是:
当 key 冲突时,如何合并两个 value
2️⃣ 常见 mergeFunction 写法
java
(a, b) -> a // 保留第一个
(a, b) -> b // 使用后者覆盖
没有 mergeFunction = 默认抛异常
3️⃣ 修复后的生产级写法
java
Map<Long, UserDTO> userMap = users.stream()
.filter(u -> u.getId() != null)
.collect(Collectors.toMap(
UserDTO::getId,
Function.identity(),
(u1, u2) -> u1
));
这段代码具备:
- ✅ key 非空保护
- ✅ key 冲突不炸
- ✅ 行为可预期
五、为什么开发环境没问题,测试却炸了?
这是 Stream 最危险的地方。
| 环境 | 数据特点 |
|---|---|
| 开发 | 数据干净、量小 |
| 测试 | 历史测试数据、脏数据、兼容数据 |
Stream 的很多异常:
- 不会在编译期暴露
- 不会在小数据集出现
- 只会在偏向真实业务数据中触发
六、生产级 Stream 编写的 5 条铁律
✅ 1️⃣ 任何 toMap,必须思考 key 是否可能重复
不能 100% 确定 → 一定要写 mergeFunction
✅ 2️⃣ 永远不要相信 key 一定非 null
java
.filter(x -> key != null)
是 Stream 中非常便宜、但非常重要的一行。
✅ 3️⃣ 不要用"看起来像 ID 的字段"
java
getUserId()
getId()
getUid()
名字相似 ≠ 语义一致
✅ 4️⃣ Stream 不是业务校验工具
Stream 不会帮你判断:
- 数据是否合理
- 字段是否完整
- 是否符合业务约束
👉 这些必须在你写 Stream 之前处理
✅ 5️⃣ 线上代码,宁可"多写一行",不要"省一行"
对比:
java
Collectors.toMap(UserDTO::getId, Function.identity())
和:
java
Collectors.toMap(
UserDTO::getId,
Function.identity(),
(a, b) -> a
)
第二种 不是啰嗦,而是专业
七、总结一句话
Stream 是表达力工具,不是容错工具。
写 Stream 时你要明确三件事:
- key 从哪里来
- value 是什么
- 发生冲突你要谁
如果这三点你没想清楚,
那这个 Stream 迟早会在线上出事故。