Comparable与Comparator
本篇定位
排序是 Java 里最常被调用、也最容易被写错的逻辑之一。Comparable 定义"自然顺序",Comparator 定义"外部策略"。TreeSet/TreeMap/Collections.sort/Stream.sorted 都靠它们工作。核心区别就一句话:Comparable 是"我天生就这么排",Comparator 是"按这个规则排"。
一、Comparable:自然顺序
类实现 Comparable<T> 接口,重写 compareTo 方法:
public class Student implements Comparable<Student> {
private String name;
private int score;
@Override
public int compareTo(Student other) {
return Integer.compare(this.score, other.score); // 按分数升序
}
}
三条契约(和 equals 一样,违反会导致排序结果不确定):
a.compareTo(b)的符号与b.compareTo(a)相反(对称性);- 传递性:
a < b && b < c→a < c; - 强烈建议
compareTo返回 0 时equals也返回 true(TreeSet 等依赖 compareTo 去重的集合,不一致会导致"逻辑上相等但集合里同时存在两个")。
返回值约定:this < other → 负数;this == other → 0;this > other → 正数。永远别用 a - b 做整数比较——溢出(Integer.MIN_VALUE - 1 变成正数)会让排序崩溃。用 Integer.compare(a, b) 或 Long.compare。
二、Comparator:外部策略
不需要修改类本身,匿名类/Lambda/方法引用即可:
// 按姓名排序
students.sort(Comparator.comparing(Student::getName));
// 按分数降序
students.sort(Comparator.comparingInt(Student::getScore).reversed());
// 多字段排序:先按分数降序,再按姓名升序
students.sort(
Comparator.comparingInt(Student::getScore).reversed()
.thenComparing(Student::getName)
);
Comparator.comparing() 是 JDK 8 引入的工厂方法,接受一个 Function 提取排序键,内部自动处理 null 安全和类型推断。不要手写 (a,b) -> a.getX() - b.getX()——溢出、null、类型推断三方面都不如 comparing 稳健。
三、Comparable vs Comparator 选择表
| 维度 | Comparable | Comparator |
|---|---|---|
| 接口 | java.lang.Comparable<T> |
java.util.Comparator<T> |
| 方法 | int compareTo(T o) |
int compare(T o1, T o2) |
| 修改 | 需修改类源码 | 不修改类,外部定义 |
| 适用 | 唯一自然顺序(如按 ID 排序) | 多策略排序(按时间/按名称/按金额) |
| 位置 | 类内部 | 调用方传入 |
一句话:如果这个类"只有一个公认的排序方式",用 Comparable;如果需要多种排序方式,或者不能改类的源码,用 Comparator。
四、与集合框架的配合
TreeSet / TreeMap 的去重逻辑:它们不靠 equals/hashCode,而是靠 compareTo/compare 返回 0 来判定"相等"。这就是第三节说的"compareTo 返回 0 时 equals 最好也返回 true"的深层原因。
// 按分数排,但分数相同就算"同一个学生"——TreeSet 里只会留一个
Set<Student> set = new TreeSet<>(Comparator.comparingInt(Student::getScore));
set.add(new Student("张三", 90));
set.add(new Student("李四", 90)); // compare 返回 0 → 被 TreeSet 认为是重复,不加入
System.out.println(set.size()); // 1
Comparable 与 Comparator 同时存在时:Comparator 优先——TreeSet(Comparator) 构造器传入的 Comparator 覆盖元素的自然顺序。
五、实战:排序的四种写法(从老到新)
List<Student> list = new ArrayList<>();
// 写法1:类实现 Comparable + Collections.sort
Collections.sort(list);
// 写法2:匿名内部类 Comparator
Collections.sort(list, new Comparator<Student>() {
public int compare(Student a, Student b) { return a.getName().compareTo(b.getName()); }
});
// 写法3:Lambda
list.sort((a, b) -> a.getName().compareTo(b.getName()));
// 写法4:方法引用 + Comparator.comparing(推荐)
list.sort(Comparator.comparing(Student::getName));
优先写 4,可读性最好;多字段排序链式 .thenComparing() 也自然。
六、高频面试题速答
- Comparable 和 Comparator 区别? 见第三节表格;核心是"内部自然顺序 vs 外部策略"。
- compareTo 返回 0 和 equals 返回 true 不一致会怎样? TreeSet/TreeMap 认为"两个元素相同"(只留一个),但 HashMap 按 equals 认为不同——同一对象在不同集合里行为不一致,是经典的 bug 来源。
- 为什么别用
a - b做 int 比较? 溢出:Integer.MIN_VALUE - 1 = Integer.MAX_VALUE(正数),排序逆序。用Integer.compare。 - Comparator.comparing 怎么处理 null? 默认不处理(null 会抛 NPE)。需要 null 安全时用
Comparator.nullsFirst(Comparator.comparing(...))或nullsLast。
勾连
- TreeSet/TreeMap 的红黑树底层:03-TreeSet / 05-TreeMap
- 排序在 Stream 里的应用:08-Stream与Optional
- Lambda 与方法引用:07-Lambda与函数式接口
⬅️ 04-Java时间新API 🏠 00-Java ➡️ 01-继承与多态
💬 评论