比较器 Comparable 与 Comparator
本教程共 100 篇 · 第 82 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
本节目标:分清 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,直接能排
compareTo 的返回值约定:小于返回负数,等于返回 0,大于返回正数。String、Integer 这些 JDK 类早就实现好了,所以字符串列表、数字列表能直接排序。
Note写
compareTo别用this.value - o.value相减——数值很大时会整数溢出、比出错误结果。一律用Integer.compare(a, b)这种安全写法。
Comparator:临时定制的比较规则
有些时候你不想(或不能)改类本身,只想这次排序按别的规则来,比如按字符串长度排。这时用 Comparator:
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);
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:
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 排最后
数据可能含 null 时,这个比自己写 if 判断干净,也少了出错机会。
和 Stream 排序呼应
List.sort 和 Stream.sorted 底层都靠 Comparable/Comparator。学好本章,日后 list.stream().sorted(Comparator.comparing(...)) 这种写法你一眼就懂,不用现查。比较器是 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。下一章进入泛型,理解这套类型安全机制是怎么来的。