---
title: "Java TreeSet 为什么漏了一条任务？检查比较器何时返回 0"
date: 2026-11-13
slug: java-treeset-comparator-identity
canonical: https://kernelranch.com/blog/2026/11/java-treeset-comparator-identity
markdown_url: https://kernelranch.com/blog/2026/11/java-treeset-comparator-identity.md
tags: ["java", "collections", "backend"]
---Java 调度队列把不同任务放进 `TreeSet` 后只剩一条时，检查比较器是否只比较优先级。对 `TreeSet` 来说，比较结果为 0 就视作同一个元素，`add()`、`contains()` 和 `remove()` 都会受影响。

## 1. 重现入队、查找和移除

将代码保存为 `TreeSetLab.java`，用 JDK 17 运行 `java TreeSetLab.java`。

```java
import java.util.Comparator;
import java.util.TreeSet;

class TreeSetLab {
    record Job(int priority, String id) {}

    static void require(boolean condition) {
        if (!condition) {
            throw new AssertionError("unexpected TreeSet result");
        }
    }

    public static void main(String[] args) {
        Job first = new Job(10, "job-a");
        Job second = new Job(10, "job-b");
        require(!first.equals(second));

        Comparator<Job> byPriority = Comparator.comparingInt(Job::priority);
        var broken = new TreeSet<>(byPriority);
        require(broken.add(first));
        boolean added = broken.add(second);
        boolean found = broken.contains(second);
        boolean removed = broken.remove(second);
        require(!added && found && removed && broken.isEmpty());
        System.out.printf(
            "priority only: add=%b contains=%b remove=%b remaining=%d%n",
            added, found, removed, broken.size()
        );

        var ordered = new TreeSet<>(byPriority.thenComparing(Job::id));
        require(ordered.add(second) && ordered.add(first));
        require(!ordered.add(new Job(10, "job-a")));
        var ids = ordered.stream().map(Job::id).toList();
        require(ids.equals(java.util.List.of("job-a", "job-b")));
        System.out.println("priority then id: " + ids);
    }
}
```

```text
priority only: add=false contains=true remove=true remaining=0
priority then id: [job-a, job-b]
```

## 2. 先确定什么叫同一条任务

样本的 `Job` 是不可变 record，`equals()` 比较优先级和 ID。补上 ID 后，比较器也用这两个字段确定等价关系。此时任务优先级变化意味着新的排序键。若业务把 ID 当唯一身份，更新优先级时应先找到旧条目，再更新有序索引。可以由 `Map<Id, Job>` 维护身份，通过旧对象移除 `TreeSet` 条目后插入新对象。

已有条目若持有可变字段，原地修改参与比较的值会破坏集合的顺序假设。排查时可为同优先级不同 ID、同 ID 更新优先级和删除不存在的任务分别写用例。前两种用例覆盖了排序键与业务身份的边界，第三种能发现删除误命中同优先级任务。

## 小结

`TreeSet` 用比较器同时决定顺序和集合等价关系。比较器返回 0 的条件应和集合中元素的等价定义一致。

## 数据来源与检索时间

| 来源 | URL | 检索日期 | 对应内容 |
|---|---|---|---|
| Java 17 TreeSet | https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/TreeSet.html | 2026-11-13 | 比较等价关系和 Set 的约定 |
| Java 17 Comparator | https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Comparator.html | 2026-11-13 | 与 equals 一致的定义以及 thenComparing |

## 核验说明

- Windows、OpenJDK 17.0.19 本机实测两个任务的 add、contains、remove 与组合排序。示例是单线程集合操作。
