《Java Stream 中 toMap 的生产级用法:一次 Duplicate key 的异常问题复盘》

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 时你要明确三件事:

  1. key 从哪里来
  2. value 是什么
  3. 发生冲突你要谁

如果这三点你没想清楚,

那这个 Stream 迟早会在线上出事故


相关推荐
RMB Player几秒前
Spring Boot 集成飞书推送超详细教程:文本消息、签名校验、封装工具类一篇搞定
java·网络·spring boot·后端·spring·飞书
重庆小透明6 分钟前
【搞定面试之mysql】第三篇 mysql的锁
java·后端·mysql·面试·职场和发展
RuoyiOffice14 分钟前
企业请假销假系统设计实战:一张表、一套流程、两段生命周期——BPM节点驱动的表单变形术
java·spring·uni-app·vue·产品运营·ruoyi·anti-design-vue
鹤旗15 分钟前
While语句,do-while语句,for语句
java·jvm·算法
小碗羊肉25 分钟前
【从零开始学Java | 第十八篇】BigInteger
java·开发语言·新手入门
sky wide35 分钟前
[特殊字符] Docker Swarm 集群搭建指南
java·docker·容器
wuqingshun31415940 分钟前
谈谈你对springAop动态代理的理解?
java·jvm
执笔画流年呀42 分钟前
PriorityQueue(堆)续集
java·开发语言
武超杰1 小时前
Spring Boot入门教程
java·spring boot·后端
左左右右左右摇晃1 小时前
JDK 1.7 ConcurrentHashMap——分段锁
java·开发语言·笔记