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 一样,违反会导致排序结果不确定):

  1. a.compareTo(b) 的符号与 b.compareTo(a) 相反(对称性);
  2. 传递性:a < b && b < ca < 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/方法引用即可:

// 按姓名排序
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() 也自然。

六、高频面试题速答

  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

勾连


⬅️ 04-Java时间新API 🏠 00-Java ➡️ 01-继承与多态