--- title: "05-Comparable与Comparator" created: 2026-09-03 tags: - Java --- # Comparable与Comparator > [!note] 本篇定位 > 排序是 Java 里最常被调用、也最容易被写错的逻辑之一。`Comparable` 定义"自然顺序",`Comparator` 定义"外部策略"。TreeSet/TreeMap/Collections.sort/Stream.sorted 都靠它们工作。核心区别就一句话:**Comparable 是"我天生就这么排",Comparator 是"按这个规则排"**。 ## 一、Comparable:自然顺序 类实现 `Comparable` 接口,重写 `compareTo` 方法: ```java public class Student implements Comparable { private String name; private int score; @Override public int compareTo(Student other) { return Integer.compare(this.score, other.score); // 按分数升序 } } ``` 三条契约(和 `equals` 一样,违反会导致排序结果不确定): 1. `a.compareTo(b)` 的符号与 `b.compareTo(a)` 相反(对称性); 2. 传递性:`a < b && b < c` → `a < c`; 3. **强烈建议** `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/方法引用即可: ```java // 按姓名排序 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` | `java.util.Comparator` | | 方法 | `int compareTo(T o)` | `int compare(T o1, T o2)` | | 修改 | 需修改类源码 | 不修改类,外部定义 | | 适用 | 唯一自然顺序(如按 ID 排序) | 多策略排序(按时间/按名称/按金额) | | 位置 | 类内部 | 调用方传入 | 一句话:**如果这个类"只有一个公认的排序方式",用 Comparable;如果需要多种排序方式,或者不能改类的源码,用 Comparator**。 ## 四、与集合框架的配合 **TreeSet / TreeMap 的去重逻辑**:它们不靠 `equals`/`hashCode`,而是靠 `compareTo`/`compare` 返回 0 来判定"相等"。这就是第三节说的"compareTo 返回 0 时 equals 最好也返回 true"的深层原因。 ```java // 按分数排,但分数相同就算"同一个学生"——TreeSet 里只会留一个 Set 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 覆盖元素的自然顺序。 ## 五、实战:排序的四种写法(从老到新) ```java List list = new ArrayList<>(); // 写法1:类实现 Comparable + Collections.sort Collections.sort(list); // 写法2:匿名内部类 Comparator Collections.sort(list, new Comparator() { 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()` 也自然。 ## 六、高频面试题速答 1. **Comparable 和 Comparator 区别?** 见第三节表格;核心是"内部自然顺序 vs 外部策略"。 2. **compareTo 返回 0 和 equals 返回 true 不一致会怎样?** TreeSet/TreeMap 认为"两个元素相同"(只留一个),但 HashMap 按 equals 认为不同——同一对象在不同集合里行为不一致,是经典的 bug 来源。 3. **为什么别用 `a - b` 做 int 比较?** 溢出:`Integer.MIN_VALUE - 1 = Integer.MAX_VALUE`(正数),排序逆序。用 `Integer.compare`。 4. **Comparator.comparing 怎么处理 null?** 默认不处理(null 会抛 NPE)。需要 null 安全时用 `Comparator.nullsFirst(Comparator.comparing(...))` 或 `nullsLast`。 ## 勾连 - TreeSet/TreeMap 的红黑树底层:[[03-TreeSet|03-TreeSet]] / [[05-TreeMap|05-TreeMap]] - 排序在 Stream 里的应用:[[08-Stream与Optional|08-Stream与Optional]] - Lambda 与方法引用:[[07-Lambda与函数式接口|07-Lambda与函数式接口]] --- ⬅️ [[04-Java时间新API|04-Java时间新API]] 🏠 [[00-Java|00-Java]] ➡️ [[01-继承与多态|01-继承与多态]]