Comparable 与 Comparator 比较器 | JavaSE
Comparable 与 Comparator 比较器
一、学习目标
完成本章后,你应该能够:
- 能够解释为什么 TreeSet、TreeMap 和排序操作必须获得明确的比较规则。
- 能够区分自然排序(Natural Ordering)与比较器排序(Comparator Ordering)。
- 能够独立让自定义类实现
Comparable<T>并正确重写compareTo()。 - 能够使用
Comparator<T>为同一种对象定义不同排序规则。 - 能够准确解释比较结果为负数、0、正数的语义。
- 能够设计“主条件 + 次条件 + 第三条件”的多字段排序规则。
- 能够解释为什么 TreeSet 中
compare(...) == 0会直接影响元素去重。 - 能够避免直接使用整数相减实现比较所产生的溢出风险。
- 能够判断何时应该使用 Comparable,何时更适合 Comparator。
二、核心知识
2.1 为什么需要比较规则
上一章已经发现:
TreeSet<Student> students = new TreeSet<>();
如果 Student 没有任何比较能力,那么 TreeSet 会面临一个问题:
张三应该放在李四前面还是后面?
这不是 Java 能够自动决定的。
例如 Student:
class Student {
private String name;
private int age;
private double score;
}
至少可以设计出很多排序方法:
按年龄升序
按年龄降序
按成绩升序
按成绩降序
按姓名字典序
按成绩降序,成绩相同再按年龄升序
所以:
排序的本质,是先定义对象之间的大小关系。
Java 为此提供两套核心机制:
Comparable
Comparator
2.2 Comparable:对象自己的自然排序规则
Comparable<T> 位于:
java.lang
因此不需要手动导包。
它的核心方法:
int compareTo(T other);
如果:
class Student implements Comparable<Student>
就意味着 Student 类在告诉 Java:
“Student 对象自己知道 Student 与 Student 应该怎样比较。”
这套规则称为:
自然排序(Natural Ordering)
2.3 Comparable 的基本结构
例如规定:
Student 默认按照年龄升序排列。
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
}
之后:
TreeSet<Student> students = new TreeSet<>();
TreeSet 就能够调用:
student1.compareTo(student2)
判断两个 Student 的先后顺序。
2.4 compareTo 返回值是什么意思
不要死记必须返回:
-1
0
1
Java 真正规定的是:
负整数
0
正整数
含义如下:
| 返回结果 | 含义 |
| -------- | ---------------------------- |
| < 0 | 当前对象小于参数对象 |
| == 0 | 两个对象在这套比较规则下等价 |
| > 0 | 当前对象大于参数对象 |
例如:
Integer.compare(this.age, other.age);
如果:
this.age = 18
other.age = 20
得到负数,说明:
当前对象应排在 other 前面
2.5 为什么不推荐直接相减
初学代码经常写:
return this.age - other.age;
对于年龄这种小范围数据,通常看不出问题。
但是假设:
int a = Integer.MAX_VALUE;
int b = -1;
执行:
a - b
就可能产生整数溢出。
因此正式代码更推荐:
Integer.compare(a, b);
对于:
long
使用:
Long.compare(a, b);
对于:
double
使用:
Double.compare(a, b);
这样更加可靠。
2.6 Comparator:从对象外部提供比较规则
Comparable 的问题在于:
比较规则被定义在类自己内部。
但是现实业务中,同一种对象可能存在很多种排序方式。
例如 Student:
学生列表页面
→ 按年龄排序
成绩排行榜
→ 按成绩排序
学生通讯录
→ 按姓名排序
如果只能在 Student 中定义一个:
compareTo()
显然不够灵活。
于是 Java 提供:
Comparator<T>
Comparator 位于:
java.util
核心方法:
int compare(T o1, T o2);
它表达的是:
由集合、排序操作或调用方从对象外部提供一套比较规则。
2.7 Comparator 基本写法
例如 Student 本身没有实现 Comparable:
public class Student {
private String name;
private int age;
private double score;
// 构造器、getter、toString...
}
创建 TreeSet 时指定:
Comparator<Student> comparator = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return Double.compare(o2.getScore(), o1.getScore());
}
};
TreeSet<Student> students = new TreeSet<>(comparator);
这里表示:
按照成绩降序
因为比较顺序反过来写成:
Double.compare(o2.getScore(), o1.getScore())
2.8 Comparable 与 Comparator 的核心区别
| 对比项 | Comparable | Comparator |
| -------------------------- | ---------------- | --------------------- |
| 包 | java.lang | java.util |
| 核心方法 | compareTo(T o) | compare(T o1, T o2) |
| 比较规则位置 | 类内部 | 类外部 |
| 表达含义 | 自然排序 | 外部指定排序 |
| 是否修改原类 | 通常需要 | 不需要 |
| 一个类能否方便拥有多种规则 | 不方便 | 非常适合 |
| TreeSet 使用方式 | 无参构造即可 | 构造器传入 Comparator |
可以简单理解成:
Comparable
= 我自己规定“我天生应该怎样排序”
Comparator
= 别人规定“这次我要怎样排序”
2.9 如果两种规则同时存在怎么办
假设:
class Student implements Comparable<Student>
Student 自己规定:
年龄升序
但创建 TreeSet 时又传:
Comparator<Student>
规定:
成绩降序
那么:
TreeSet<Student> students =
new TreeSet<>(scoreComparator);
此时使用的是:
TreeSet 构造时显式提供的 Comparator。
可以理解成:
显式 Comparator
优先于
元素自己的自然排序
而:
new TreeSet<>()
没有 Comparator 时,才使用元素的自然排序。
三、使用方法
3.1 Comparable:按年龄升序
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
}
测试:
TreeSet<Student> students = new TreeSet<>();
students.add(new Student("张三", 20));
students.add(new Student("李四", 18));
students.add(new Student("王五", 22));
TreeSet 使用:
compareTo()
维护自然顺序。
3.2 一个非常严重的问题:年龄相同怎么办
假设:
张三 20
李四 20
而:
compareTo()
只有:
return Integer.compare(this.age, other.age);
那么:
20 与 20
↓
compareTo == 0
TreeSet 会认为:
两个元素在当前排序规则中等价。
于是第二个学生可能无法进入集合。
这就是:
排序规则同时决定 TreeSet 的元素唯一性。
3.3 多条件排序
如果需求是:
年龄升序
年龄相同时按姓名排序
可以:
@Override
public int compareTo(Student other) {
int result = Integer.compare(this.age, other.age);
if (result != 0) {
return result;
}
return this.name.compareTo(other.name);
}
逻辑是:
先比较年龄
↓
年龄不同
→ 直接得到结果
年龄相同
↓
继续比较姓名
这就是:
多级排序(Multi-level Ordering)
3.4 三个排序条件
例如需求:
成绩降序
成绩相同 → 年龄升序
年龄相同 → 姓名字典序
可以写:
Comparator<Student> comparator = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int result =
Double.compare(o2.getScore(), o1.getScore());
if (result != 0) {
return result;
}
result =
Integer.compare(o1.getAge(), o2.getAge());
if (result != 0) {
return result;
}
return o1.getName().compareTo(o2.getName());
}
};
然后:
TreeSet<Student> students =
new TreeSet<>(comparator);
这是你必须真正掌握的比较器思维。
3.5 Comparator 的 Lambda 写法
因为 Comparator 是函数式接口,所以可以写:
Comparator<Student> comparator =
(o1, o2) ->
Double.compare(
o2.getScore(),
o1.getScore()
);
本章重点是理解:
Comparator 在表达什么比较规则
Lambda 的系统语法、省略规则和进一步实战会在后面的 Lambda 专章继续学习。
3.6 Comparator 的现代组合能力
Java 提供了一系列 Comparator 工具方法。
例如:
Comparator<Student> comparator =
Comparator.comparingDouble(
student -> student.getScore()
);
还可以继续组合:
Comparator<Student> comparator =
Comparator
.comparingDouble(
student -> student.getScore()
)
.thenComparingInt(
student -> student.getAge()
);
其中:
thenComparing(...)
表示:
前面的比较条件无法区分两个对象时,再使用下一条件。
这和我们手写:
if (result != 0) {
return result;
}
本质上解决同一个问题。
3.7 reversed()
Comparator 还可以:
comparator.reversed();
得到相反顺序的 Comparator。
例如:
Comparator<Integer> comparator =
Integer::compare;
反向后就是对应的逆序。
这里先理解 API 能力即可,方法引用的完整语法将在后面专门学习。
四、原理与进阶
4.1 Comparable 表达的是全序关系
Comparable 并不是:
随便返回一个正负数就结束
一个正确的比较规则应该保持逻辑稳定。
例如:
A > B
B > C
那么正常应该有:
A > C
这称为:
传递性(Transitivity)
否则 TreeSet、排序算法等结构就无法可靠工作。
4.2 反向比较必须保持一致
如果:
compare(A, B) > 0
那么:
compare(B, A)
应该具有相反符号。
不能出现:
A > B
同时
B > A
这种自相矛盾的比较规则。
4.3 compare == 0 的含义非常重要
如果:
compare(a, b) == 0
就表示:
a 和 b 在当前比较关系中属于同一个排序等价类。
对于:
TreeSet
TreeMap
这个结果会直接影响:
是否认为元素/键已经存在
所以 Comparator 不能只考虑:
“排出来看着顺眼”
还必须考虑:
“返回 0 会不会错误吞掉合法对象”
4.4 排序最好与 equals 保持一致
Java 官方建议:
compare(a, b) == 0
最好和:
a.equals(b)
表达相同的逻辑相等关系。
也就是:
比较认为相同
≈
equals 也认为相同
否则:
HashSet
与:
TreeSet
对同一组对象的“重复”判断可能出现不同结果。
这并不意味着所有不一致设计都无法运行,而是这种设计容易导致违反一般 Set 语义直觉的行为。
4.5 排序字段必须谨慎设计
例如 User:
id
name
score
如果 Comparator 只有:
Double.compare(u2.score, u1.score)
那么所有分数相同的用户都会:
compare == 0
放进 TreeSet 时就可能只保留其中一个。
如果业务要求:
同分用户必须全部存在。
就要增加可以继续区分用户的字段,例如:
score
↓
name
↓
id
最终保证真正不同的用户不会因为排序条件过少而被错误视为相同。
4.6 Comparable 更适合稳定的自然顺序
例如:
日期
数字
字符串
版本号
这些对象通常存在一个相对自然、稳定的默认顺序。
此时 Comparable 很合理。
但是:
Student
Product
User
Order
这类业务对象可能需要:
年龄排序
成绩排序
价格排序
创建时间排序
销量排序
名称排序
这种情况下 Comparator 通常更灵活。
五、实践应用
5.1 成绩排行榜
需求:
成绩降序
成绩相同按学号升序
非常适合:
Comparator<Student>
因为“成绩排名”通常只是 Student 的某一个业务视图,而不一定代表 Student 的天然顺序。
5.2 商品列表
Product:
name
price
sales
rating
前端可能要求:
价格升序
价格降序
销量降序
评分降序
这些本质上就是:
同一种对象对应多套 Comparator。
5.3 Comparable 与 Comparator 选型
可以先使用下面的判断法:
这个顺序是不是对象稳定、天然、默认的顺序?
↓
是
↓
Comparable
需要临时排序?
需要多套排序?
不能修改原类?
↓
是
↓
Comparator
六、常见问题
6.1 compareTo 必须只返回 -1、0、1 吗?
不是。
只要求:
负数
0
正数
分别表达:
小于
等于
大于
6.2 为什么不推荐直接 o1.age - o2.age?
因为整数相减可能发生:
overflow
更安全:
Integer.compare(o1.getAge(), o2.getAge());
6.3 Comparable 与 Comparator 谁更高级?
不存在谁“更高级”。
它们解决的问题不同:
Comparable
→ 自然顺序
Comparator
→ 外部指定顺序
6.4 一个类可以有几个 Comparable 规则?
从:
compareTo()
这个自然比较入口来看,一个类通常只能定义一套自然排序。
但可以创建任意多套:
Comparator
6.5 TreeSet 同时存在 Comparable 和 Comparator 时用谁?
如果创建 TreeSet 时显式传入 Comparator:
new TreeSet<>(comparator)
使用 Comparator。
如果没有:
new TreeSet<>()
则使用元素的自然排序。
6.6 为什么我的 TreeSet 少了几个对象?
第一件事就检查:
compareTo()
或者:
Comparator.compare()
是不是对两个业务上不同的对象返回了:
0
因为 TreeSet 会因此认为它们在排序意义上相同。
6.7 Comparator 能用于 List 吗?
可以。
Comparator 并不是 TreeSet 专属。
它也可以用于:
List 排序
数组排序
TreeSet
TreeMap
其他需要排序规则的 API
本章先围绕集合排序建立模型即可。
七、练习与验收
7.1 知识问答
- Comparable 解决什么问题?
- Comparator 解决什么问题?
- 什么叫自然排序?
compareTo()返回负数、0、正数分别代表什么?compare()与compareTo()的参数数量为什么不同?- Comparable 与 Comparator 的核心区别是什么?
- TreeSet 同时存在 Comparator 和 Comparable 时使用哪一个?
- 为什么不推荐直接使用整数相减完成比较?
- 什么叫多级排序?
- 为什么比较结果为 0 会影响 TreeSet 去重?
- 为什么排序关系最好与 equals 保持一致?
- 什么场景更适合 Comparable?
- 什么场景更适合 Comparator?
- Comparator 为什么特别适合“一种对象,多种排序”?
7.2 代码阅读
阅读:
@Override
public int compareTo(Student other) {
int result =
Integer.compare(this.age, other.age);
if (result != 0) {
return result;
}
return this.name.compareTo(other.name);
}
回答:
- 第一排序条件是什么?
- 升序还是降序?
- 什么情况下继续比较姓名?
- 如果年龄和姓名都相同,会返回什么?
- 放入 TreeSet 后意味着什么?
7.3 手写代码
任务一:Comparable
定义:
Student
name
age
要求:
年龄升序
年龄相同按姓名排序
让 Student 自己实现 Comparable。
任务二:Comparator
Student:
name
age
score
要求创建 Comparator:
成绩降序
成绩相同时年龄升序
任务三:三级排序
实现:
成绩降序
↓
年龄升序
↓
姓名字典序
至少构造:
- 两个成绩相同对象;
- 两个年龄也相同对象;
验证比较规则能够继续向下区分。
7.4 Debug
下面代码:
Comparator<Student> comparator =
(s1, s2) -> (int) (s2.getScore() - s1.getScore());
回答:
- 为什么这种写法存在问题?
- 当两个成绩只差
0.5时,强转成 int 可能发生什么? - 为什么可能错误返回 0?
- TreeSet 收到错误的 0 后会发生什么?
- 应该使用什么 API 改写?
7.5 综合训练
设计学生排行榜。
Student:
studentNo
name
age
score
规则:
成绩降序
成绩相同 → 年龄升序
年龄相同 → 姓名字典序
姓名相同 → 学号升序
要求:
- 使用 Comparator。
- 使用 TreeSet。
- 添加至少 8 个学生。
- 故意设计多个同分学生。
- 保证不同学生不会因为 Comparator 返回 0 被误删。
- 分析如果删除最后的学号比较条件,会产生什么风险。
7.6 本章验收
- [ ] 能独立解释 Comparable 与 Comparator。
- [ ] 能从零实现
compareTo()。 - [ ] 能从零实现
Comparator.compare()。 - [ ] 能解释负数、0、正数的语义。
- [ ] 不再使用简单减法作为默认整数比较方案。
- [ ] 能写两字段比较规则。
- [ ] 能写三字段以上比较规则。
- [ ] 能解释
compare == 0对 TreeSet 的影响。 - [ ] 能判断 Comparable 与 Comparator 的使用场景。
- [ ] 能解释显式 Comparator 为什么覆盖自然排序规则。