Java Collectors.toMap 踩坑:重复 key 抛异常、null 值炸 NPE 与合并策略
Java Collectors.toMap 踩坑:重复 key 抛异常、null 值炸 NPE 与合并策略list.stream().collect(Collectors.toMap(...))是把列表转成 Map 的常用写法,但它藏着三个线上高频崩溃点:key 重复直接抛异常、value 为 null 抛 NPE、以及不小心丢了顺序。这几个坑本地小数据量根本测不出来,一到生产环境数据一杂就炸。这篇把它们讲透。坑一:重复 key 直接抛 IllegalStateException先看最朴素的写法,把用户列表转成「id → 用户」的 Map:recordUser(Longid,Stringname){}ListUserusersList.of(newUser(1L,Alice),newUser(2L,Bob),newUser(1L,Alice2)// 注意:id1 重复了);MapLong,Usermapusers.stream().collect(Collectors.toMap(User::id,u-u));你可能觉得后者覆盖前者就好,但实际运行会直接抛异常:java.lang.IllegalStateException: Duplicate key 1 (attempted merging values ...)这是toMap两参数版本的默认行为:只要 key 重复就抛异常,它假设你的 key 天然唯一。真实数据里 key 重复太常见了(数据没清洗、业务本身允许重复),所以这个坑几乎人人都踩。正确做法是用三参数版本,显式提供合并策略(mergeFunction):// 重复 key 时,保留后出现的那个(覆盖)MapLong,Usermapusers.stream().collect(Collectors.toMap(User::id,u-u,(existing,replacement)-replacement// 合并策略:取新值));// 或者保留先出现的:// (existing, replacement) - existingmergeFunction的两个参数分别是「已存在的值」和「新来的值」,返回哪个就用哪个。你甚至可以做聚合,比如把重名的合并:// 按 name 分组,重复时把 id 拼起来MapString,StringbyNameusers.stream().collect(Collectors.toMap(User::name,u-String.valueOf(u.id()),(a,b)-a,b// 1,1));经验法则:凡是不能 100% 保证 key 唯一,就无脑用三参数版本,别赌数据干净。坑二:value 为 null 抛 NPE第二个更隐蔽。假设 value 可能是 null:MapLong,Stringmapusers.stream().collect(Collectors.toMap(User::id,u-u.name().equals(Bob)?null:u.name()// Bob 的 value 是 null));这行会抛NullPointerException,而且报错信息很误导人。原因是toMap底层用Map.merge,而merge明确规定 value 不能为 null。注意:这和HashMap.put(k, null)不同——put允许 null 值,但toMap不允许,很多人以为一样。如果你确实需要保留 null 值,得放弃toMap,改用Collectors.toMap之外的方式,比如forEach手动塞,或者用reduce/groupingBy。最简单的是自己收集:MapLong,StringmapnewHashMap();users.forEach(u-map.put(u.id(),u.name().equals(Bob)?null:u.name()));// HashMap.put 允许 null 值,不会炸或者干脆在流里过滤掉 null,让语义更清晰:MapLong,Stringmapusers.stream().filter(u-!u.name().equals(Bob))// 先剔除会产生 null 的.collect(Collectors.toMap(User::id,User::name));坑三:结果 Map 无序toMap默认返回HashMap,遍历顺序和插入顺序无关。如果你需要保持原列表顺序(比如后面要按顺序输出),得用四参数版本指定 map 类型:importjava.util.LinkedHashMap;MapLong,Userorderedusers.stream().collect(Collectors.toMap(User::id,u-u,(a,b)-b,LinkedHashMap::new// 第四个参数:指定用 LinkedHashMap 保序));四参数版本必须连 mergeFunction 一起给(没法跳过),这是 API 设计的硬性要求。需要保序就记得用LinkedHashMap::new,需要排序可以用TreeMap::new。小结重复 key→ 用三参数toMap,显式给 mergeFunction((a,b)-b覆盖 /(a,b)-a保留旧值),别用两参数版本赌数据唯一。value 为 null→toMap底层用merge,禁止 null 值;要保留 null 就改用HashMap.put手动收集或先 filter 掉。要保序→ 用四参数版本传LinkedHashMap::new,默认的 HashMap 不保证顺序。一句话记忆:Collectors.toMap的两参数版本是「乐观版」,只适合玩具数据;生产代码一律上三参数(带合并策略),value 可能为 null 时干脆别用它。