DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

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 迭代顺序,也不要据此假定结果容器线程安全。

保留改动前的校验与收集代码。若合并策略不符合业务,回退代码后重新处理仍需确认此前已发生的外部操作,不能把再次收集当作自动回滚。