← 返回题目列表

Comparable 和 Comparator 有什么区别?

高频 简单 第 2 / 30 题 更新于 2026/07/26
ComparableComparator排序

简化版

两个都是排序用的接口,区别在谁来定义排序规则Comparable 是「内部比较」,类自己实现 compareTo(),定义它的天生默认顺序(如 String 按字典序);Comparator 是「外部比较」,单独写一个比较器,不改原类就能定义各种排序规则。要给一个类一个默认排序用 Comparable,要多种/临时排序规则用 Comparator。

详细版

Comparable(java.lang)—— 内比较,实现 compareTo

class User implements Comparable<User> {
    int age;
    public int compareTo(User o) {
        return Integer.compare(this.age, o.age);   // 定义「User 天生按年龄排」
    }
}
Collections.sort(userList);   // 直接用类自带的顺序

Comparator(java.util)—— 外比较,实现 compare

// 不动 User 类,临时定义按名字排
userList.sort(Comparator.comparing(User::getName));
// 也能按年龄降序、组合排序
userList.sort(Comparator.comparingInt(User::getAge).reversed());
userList.sort(Comparator.comparing(User::getName).thenComparingInt(User::getAge));
维度ComparableComparator
java.langjava.util
方法compareTo(o)(一个参数)compare(a, b)(两个参数)
谁定义规则自己内部外部单独定义
改不改原类要改(implements)不改原类
排序规则数只能一种(默认顺序)可以有任意多种

完整版教学

一、核心区别:内部默认 vs 外部灵活

抓住一个字:Comparable 是「内」,Comparator 是「外」。

  • Comparable 写在类的内部,回答「这个类默认怎么排」。一个类只能有一种 compareTo,就是它的天生顺序。像 IntegerStringLocalDate 都实现了 Comparable,所以它们能直接 sort
  • Comparator外挂的比较器,回答「这一次我想怎么排」。你可以为同一个类写无数个 Comparator(按年龄、按名字、按薪资降序……),而且不用碰原类的源码

二、为什么需要 Comparator——两个 Comparable 解决不了的场景

  1. 一个类需要多种排序User 有时按年龄排、有时按名字排。Comparable 只能定义一种默认顺序,多出来的规则只能用 Comparator。
  2. 改不了源码的类:比如第三方库的类、或你不想污染的类,没法给它加 implements Comparable,只能从外部用 Comparator 定规则。

记忆点:Comparable 给类一个「天生的默认顺序」,Comparator 给你「临时、多样、不侵入」的排序能力。

三、compareTo / compare 的返回值约定

两者返回值规则一样,都是三态整数

  • 返回负数:当前元素排在前面(this < o);
  • 返回 0:两者相等,顺序不变;
  • 返回正数:当前元素排在后面(this > o)。
return Integer.compare(a, b);   // ✅ 推荐,安全
return a - b;                   // ⚠️ 有溢出风险!大正数减大负数会溢出,别用

⚠️ 别用 a - b 做比较——当 a、b 是很大的正负数时,减法会整数溢出导致结果符号错误。用 Integer.compare(a, b)Long.compare

四、Java 8 让 Comparator 变得极其好用

Java 8 给 Comparator 加了一堆静态/默认方法,链式组合非常方便:

list.sort(
    Comparator.comparing(User::getDept)          // 先按部门
              .thenComparing(User::getName)       // 部门相同再按名字
              .thenComparing(User::getAge,
                    Comparator.reverseOrder())     // 再按年龄降序
);

comparingthenComparingreversednullsFirst 等让多级排序、降序、空值处理都变成一行链式调用,几乎取代了手写匿名内部类。

五、排序契约为什么会影响 Set 和 Map

比较器不只服务 sort,TreeSet 与 TreeMap 还把“比较结果为 0”当作键相同。如果 Comparator 只比较年龄,两个姓名不同但同为 20 岁的 User 会被 TreeSet 视为重复,第二个元素无法加入;这不一定是 bug,但必须与业务唯一性一致。

Comparator<User> byAge = Comparator.comparingInt(User::getAge);
Set<User> users = new TreeSet<>(byAge);
users.add(new User("A", 20));
users.add(new User("B", 20));
System.out.println(users.size()); // 1,比较器认为二者等价

比较关系还应满足反对称、传递和稳定性。若 A < B、B < C,却得到 A > C,排序算法可能抛出“comparison method violates its general contract”,有序容器也可能查找异常。自然顺序最好与 equals() 一致,否则“HashSet 有两个、TreeSet 只有一个”会让 API 使用者困惑。

契约要求破坏后的风险
符号反对称sign(compare(a,b)) = -sign(compare(b,a))前后比较矛盾
传递性a>b 且 b>c,则 a>c排序失败或顺序不稳定
等价一致compare(a,b)=0 时,对其他元素比较符号一致TreeSet/TreeMap 行为异常
与 equals 一致推荐但非绝对强制不同集合的“重复”语义不同

六、常见误区与追问

  • 误区:compare 只需返回 -1、0、1。 契约只关心负、零、正,返回其他绝对值也合法。
  • 误区:用 a - b 判断整数顺序。 极值相减可能溢出并反转符号,应使用 Integer.compare
  • 误区:Comparator 返回 0 只影响显示顺序。 TreeSet/TreeMap 会把它当成同一个排序键,直接影响元素数量。
  • 追问:Comparable 与 equals 必须一致吗? 规范强烈建议自然顺序与 equals 一致,否则有序集合和哈希集合的去重结果不同。
  • 追问:null 怎么排序? 自然顺序通常不接受 null,可用 Comparator.nullsFirstnullsLast 包装明确策略。
  • 追问:多字段排序怎样避免手写分支?comparingthenComparingreversed 组合,并为可能为空的字段配置 null 策略。

记忆钩子:比较器不仅决定“谁在前”,在树形集合里还决定“谁算同一个键”。

比较规则还应尽量保持稳定:若 Comparator 读取会在排序过程中变化的字段,同一对对象前后可能得到不同结果,从而破坏排序契约;参与排序的关键字段最好在一次排序或作为 TreeMap 键期间保持不变。

七、加强记忆

Comparable 把唯一的自然顺序放进类型内部,Comparator 在外部提供可组合、多场景且不侵入的规则。实现时不要做可能溢出的减法,要保证反对称、传递和等价一致性;用于 TreeSet/TreeMap 时,返回 0 还承担去重语义。默认顺序稳定且普适才实现 Comparable,临时、多字段或第三方类型排序使用 Comparator。