比较器 Comparable 与 Comparator
本教程共 100 篇 · 第 82 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
82. 比较器 Comparable 与 Comparator
本节目标:分清 Comparable(类自己的自然顺序)和 Comparator(外部临时比较规则),学完能写出可靠比较器,并会用 Java 8 的静态工厂方法简化排序。
为什么需要「比较」
前面 TreeSet、TreeMap、Collections.sort 都离不开「比大小」。Java 靠两个接口来表达「怎么比」:Comparable 和 Comparator。一个在类内部定义,一个在类外部临时给。
先记住一句总纲:Comparable 是「我和别人比」,Comparator 是「我拿这两个比」。前者写在类里,后者写成一个独立对象。
Comparable:类自己的自然顺序
让类实现 Comparable<T>,重写 compareTo,就定义了它的「自然顺序」。
record Score(int value) implements Comparable<Score> {
public int compareTo(Score o) {
return Integer.compare(this.value, o.value); // 按数值比
}
}
List<Score> list = new ArrayList<>(List.of(new Score(90), new Score(75)));
Collections.sort(list); // 因为实现了 Comparable,直接能排
```bash
`compareTo` 的返回值约定:小于返回负数,等于返回 0,大于返回正数。`String`、`Integer` 这些 JDK 类早就实现好了,所以字符串列表、数字列表能直接排序。
> [!NOTE]
> 写 `compareTo` 别用 `this.value - o.value` 相减——数值很大时会整数溢出、比出错误结果。一律用 `Integer.compare(a, b)` 这种安全写法。
## Comparator:临时定制的比较规则
有些时候你不想(或不能)改类本身,只想这次排序按别的规则来,比如按字符串长度排。这时用 `Comparator`:
```java
List<String> words = new ArrayList<>(List.of("ccc", "a", "bb"));
words.sort((a, b) -> Integer.compare(a.length(), b.length()));
System.out.println(words); // [a, bb, ccc]
Comparator 的 compare(a, b) 返回值和 compareTo 同约定。它最大的好处是不侵入原类,同一个类能有不同的比较器应对不同场景。
Tip一个类通常只有一个「自然顺序」(Comparable),但可以有无数个 Comparator。当你要的排序规则不是这个类的「天然排序」时,优先用 Comparator,而不是硬塞进 compareTo。
多字段排序
现实里常要「先按 A 比,A 相同再按 B 比」。手写很容易写错,但 Java 8 起 Comparator 提供了一串静态工厂,链式调用就行:
record User(String name, int age) {}
List<User> users = new ArrayList<>(List.of(
new User("张三", 20), new User("李四", 18), new User("张三", 25)));
users.sort(Comparator
.comparing(User::name) // 先按姓名
.thenComparing(User::age) // 同名再按年龄
.reversed()); // 整体反转
System.out.println(users);
```bash
`comparing` 取一个「排序键」,`thenComparing` 接第二、第三关键字,`reversed()` 整体倒序。比手写嵌套 if 清爽太多。
## 写 Comparator 的两个坑
第一,别手写取负来反转。有人图省事写 `return -a.compareTo(b)`,但 `Integer.MIN_VALUE` 取负还是自己,会出错。请用 `Comparator.reverseOrder()` 或 `reversed()`。
第二,排序规则要和 `equals` 兼容——凡是 `compare` 判为相等的,最好 `equals` 也相等。否则把这 Comparator 用进 TreeSet/TreeMap 当排序键时,会悄悄把「逻辑相同但 equals 不同」的元素当重复丢掉。
> [!WARNING]
> 把 Comparator 用在 TreeSet / TreeMap 时,如果比较结果为 0 但 equals 为 false,集合会认为它们是同一个键而只保留一个。这是 Tree 系集合的特殊行为,用前务必想清楚。
## 该用哪个
- 这个类「天生就该这么排」(比如分数按高低、日期按先后)→ 实现 `Comparable`,一劳永逸。
- 这次排序要换个角度、类又改不了 → 用 `Comparator` 临时指定。
- 多字段、要反转 → 用 `Comparator.comparing/thenComparing/reversed()` 链式写法。
## 实现 Comparable 的注意事项
类一旦实现 `Comparable`,就定下了「默认排序」。这个顺序要符合直觉(比如分数按高低、名字按字典),否则用 `Collections.sort` 的人会一脸懵。另外 `compareTo` 的结果必须满足「自反、对称、传递」的数学约束,否则排序会出诡异结果——Java 文档里写得细,自己写类时照着规范来。
## Comparator 还能当成变量存
比较规则复杂时,把它存成静态常量更清晰,也避免每次 `new`:
```java
static final Comparator<User> BY_AGE = Comparator.comparingInt(User::age);
// 使用处
users.sort(BY_AGE);
这样多个地方排序都能复用同一套规则,改动也只改一处,比到处写 lambda 好维护。
null 的处理
Comparable 的 compareTo 一般不允许 null(传 null 会 NPE)。Comparator 可以用 Comparator.nullsFirst / nullsLast 优雅处理 null 元素,让它排在前面或后面:
words.sort(Comparator.nullsLast(Comparator.naturalOrder())); // null 排最后
```bash
数据可能含 null 时,这个比自己写 `if` 判断干净,也少了出错机会。
## 和 Stream 排序呼应
`List.sort` 和 `Stream.sorted` 底层都靠 `Comparable`/`Comparator`。学好本章,日后 `list.stream().sorted(Comparator.comparing(...))` 这种写法你一眼就懂,不用现查。比较器是 Java 里复用率极高的小工具,值得练熟。
## 链式比较再举一例
多字段排序用链式写法很清晰。比如对用户先按城市、再按年龄、再按姓名排:
```java
users.sort(Comparator
.comparing(User::city)
.thenComparingInt(User::age)
.thenComparing(User::name));
thenComparingInt 是 thenComparing 的 int 特化版本,避免装箱、略微高效。字段越多,链得越长,比手写一堆 if/else 比较逻辑可读太多。这种写法一旦习惯,你就不会再想手写 compareTo 里的嵌套判断了。
常见错误:比较逻辑和 equals 打架
如果你用 Comparator 给 TreeSet/TreeMap 当排序依据,而它判为「相等」(返回 0)的两个对象 equals 却为 false,集合会当它们是同一个键,只留一个。所以比较器的「相等」语义最好和 equals 一致,不然数据会神秘丢失。记住这条,能省下好几小时的调试。
小提醒:自然顺序要符合直觉
Comparable 定义的是「默认排序」,务必符合大多数人的预期。比如日期类按时间先后、字符串按字典序,这些是约定俗成的。如果你把 User 的自然顺序定义成「按 id 倒序」,别人用 Collections.sort 时会非常困惑。非常规的排序请用 Comparator 显式表达,别藏在 compareTo 里,免得埋坑给队友。
小结
Comparable 写在类里定义自然顺序,Comparator 是外部临时比较规则。手写比较用 Integer.compare 避免溢出,多字段排序优先用 Java 8 的 comparing/thenComparing/reversed。下一章进入泛型,理解这套类型安全机制是怎么来的。