Java Collectors.toMap 报 IllegalStateException: Duplicate key
Java 二参数 toMap 遇到相等的映射键会抛出异常。明确唯一性或合并规则,安全合并列表,并区分收集结果与业务处理的副作用。
Java Stream 执行 collect(Collectors.toMap(...)) 时抛出 IllegalStateException: Duplicate key,即使 Map 的值已经声明为 List,也不会自动把同名键的列表合并。二参数 toMap 没有指定重复键处理规则,发现键相等就会拒绝这次收集。
本文使用 Java 8 及以上的标准 Stream API。不同 JDK 的异常文字可能不同,应核对键映射函数和输入数据,不能只靠错误消息中打印的对象推断哪个字段重复。
重复的是映射后的键
例如下面的输入中,同一个邮箱出现两次:
Map<String, List<String>> result = emails.stream()
.collect(Collectors.toMap(
Function.identity(),
email -> processEmail(email)
));
这段代码假定 processEmail 返回列表,但 Map 仍只能为同一个键保存一个映射。两个不同列表对应同一个邮箱时会冲突;两个列表内容完全相同也不会取消重复键检查。
如果键函数改为某个业务 ID、规范化后的字符串或其他属性,输入对象本身不相等,也可能生成相等的键。先查看冲突记录对应的键函数结果,再决定是修复脏数据、调整键设计,还是允许合并。
业务要求唯一时,继续拒绝重复通常比随便保留一条更合适。应把冲突记录交给数据校验流程,而不是只为了消除异常丢掉其中一条。
允许合并列表时,显式提供第三个参数
下面是完整的内存数据示例。它的约定是保留同一邮箱每次处理得到的全部结果,包含重复结果:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class MergeExample {
static List<String> processEmail(String email) {
return Arrays.asList("result-for-" + email);
}
public static void main(String[] args) {
List<String> emails = Arrays.asList(
"one@example.com", "two@example.com", "one@example.com"
);
Map<String, List<String>> result = emails.stream()
.collect(Collectors.toMap(
Function.identity(),
MergeExample::processEmail,
(left, right) -> {
List<String> merged = new ArrayList<>(left);
merged.addAll(right);
return merged;
}
));
System.out.println(result.get("one@example.com").size());
}
}
按这份示例的输入和纯内存处理逻辑,最后应打印 2。合并函数接收的是同一个键下的两个值,不是两个键。新建 ArrayList 再追加,避免直接修改处理函数返回的列表;返回值可能是固定大小或不可修改的集合。
如果业务只允许一份结果,不能原样采用这段合并代码。(left, right) -> left 会舍弃另一个值;它表达的是取舍策略,不是无损修复。需要保留全部原始记录时,也可以选择 groupingBy 得到每个键对应的记录列表,再按业务规则处理。
合并结果不会跳过重复的处理调用
三参数 toMap 仍会对输入元素进行值映射,再合并冲突值。如果 processEmail 实际发送邮件、扣费或写数据库,添加合并函数并不能让重复邮箱只执行一次这些动作。
应把“如何形成 Map”与“哪些业务操作允许执行”分开设计。不要通过在生产数据上重跑收集来验证修复;异常发生前的外部副作用也不会因 collect 失败而自动撤销。
同样,不要把任意输入流中的“左边”直接理解成最新业务记录。选最新值需要明确时间字段、相同时间的取舍及输入条件;并行或无序流程还需单独核对归约规则。上述示例刻意使用顺序列表和无外部副作用的函数。
验收冲突策略,而不仅是不再抛异常
至少用无重复键、同键同值、同键不同值和空输入检查结果。对列表合并,核对每个键的列表内容与数量;不要依赖普通 toMap 返回的 Map 迭代顺序,也不要据此假定结果容器线程安全。
保留改动前的校验与收集代码。若合并策略不符合业务,回退代码后重新处理仍需确认此前已发生的外部操作,不能把再次收集当作自动回滚。


