Comparable 和 Comparator 有什么区别?
简化版
两个都是排序用的接口,区别在谁来定义排序规则: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));
| 维度 | Comparable | Comparator |
|---|---|---|
| 包 | java.lang | java.util |
| 方法 | compareTo(o)(一个参数) | compare(a, b)(两个参数) |
| 谁定义规则 | 类自己内部 | 外部单独定义 |
| 改不改原类 | 要改(implements) | 不改原类 |
| 排序规则数 | 只能一种(默认顺序) | 可以有任意多种 |
完整版教学
一、核心区别:内部默认 vs 外部灵活
抓住一个字:Comparable 是「内」,Comparator 是「外」。
- Comparable 写在类的内部,回答「这个类默认怎么排」。一个类只能有一种
compareTo,就是它的天生顺序。像Integer、String、LocalDate都实现了 Comparable,所以它们能直接sort。 - Comparator 是外挂的比较器,回答「这一次我想怎么排」。你可以为同一个类写无数个 Comparator(按年龄、按名字、按薪资降序……),而且不用碰原类的源码。
二、为什么需要 Comparator——两个 Comparable 解决不了的场景
- 一个类需要多种排序:
User有时按年龄排、有时按名字排。Comparable 只能定义一种默认顺序,多出来的规则只能用 Comparator。 - 改不了源码的类:比如第三方库的类、或你不想污染的类,没法给它加
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()) // 再按年龄降序
);
comparing、thenComparing、reversed、nullsFirst 等让多级排序、降序、空值处理都变成一行链式调用,几乎取代了手写匿名内部类。
五、排序契约为什么会影响 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.nullsFirst或nullsLast包装明确策略。 - 追问:多字段排序怎样避免手写分支? 用
comparing、thenComparing、reversed组合,并为可能为空的字段配置 null 策略。
记忆钩子:比较器不仅决定“谁在前”,在树形集合里还决定“谁算同一个键”。
比较规则还应尽量保持稳定:若 Comparator 读取会在排序过程中变化的字段,同一对对象前后可能得到不同结果,从而破坏排序契约;参与排序的关键字段最好在一次排序或作为 TreeMap 键期间保持不变。
七、加强记忆
Comparable 把唯一的自然顺序放进类型内部,Comparator 在外部提供可组合、多场景且不侵入的规则。实现时不要做可能溢出的减法,要保证反对称、传递和等价一致性;用于 TreeSet/TreeMap 时,返回 0 还承担去重语义。默认顺序稳定且普适才实现 Comparable,临时、多字段或第三方类型排序使用 Comparator。