1. 引言
在实际开发中,很少只需要一种比较对象的方式。设想你有一个用户列表:有时你想按名字排序,有时按年龄排序,有时又按姓氏长度排序。再比如,你手上的类并非你自己编写,因而无法在其中添加 compareTo。针对这些场景,Java 提供了 Comparator 接口。
何时 Comparable 不够用
- 类不可修改(例如它来自第三方库)。
- 需要多种排序方式(按不同字段)。
- 希望将比较逻辑与类本身解耦(例如在程序的不同部分采用不同排序)。
类比
如果说 Comparable 是对象内置的“自然顺序”,那么 Comparator 就是一个外部“裁判”,能够按任意标准评价你的对象:今天按名字,明天按年龄,后天按名字长度。
2. Comparator 接口:语法与契约
接口声明
public interface Comparator<T> {
int compare(T o1, T o2);
}
方法 compare 应返回:
- 负数:如果第一个对象“小于”第二个对象。
- 0:如果二者相等。
- 正数:如果第一个对象“大于”第二个对象。
契约与 Comparable 相同,只是现在比较的是两个对象,而不是通过 compareTo 比较“当前对象”和“另一个对象”。
示例:按姓氏排序的比较器
假设有一个 Person 类:
public class Person {
private String firstName;
private String lastName;
private int age;
// 构造函数与 getter
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
}
创建一个按姓氏排序的比较器:
import java.util.Comparator;
public class LastNameComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
注意:String 的 compareTo 方法按字典序比较字符串。
3. 使用 Comparator:集合排序
使用比较器进行排序
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", "Kostetskaya", 25));
people.add(new Person("Boris", "Novak", 20));
people.add(new Person("Viktoriya", "Bell", 22));
// 按姓氏排序
Collections.sort(people, new LastNameComparator());
for (Person p : people) {
System.out.println(p.getLastName() + " " + p.getFirstName());
}
}
}
结果:
Novak Boris
Kostetskaya Anna
Bell Viktoriya
使用比较器按年龄排序
即使类已经按姓名实现了 Comparable,也可以再创建一个按年龄的比较器:
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
用法同上:
Collections.sort(people, new AgeComparator());
结果:
Boris Novak (20)
Viktoriya Bell (22)
Anna Kostetskaya (25)
示例:“临时”选择比较器
Collections.sort(people, new LastNameComparator()); // 按姓氏
Collections.sort(people, new AgeComparator()); // 按年龄
4. 匿名类与 Lambda 表达式
可以“临时”创建比较器,而无需声明单独的类。
匿名类
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
Lambda 表达式
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
或者使用列表方法 List.sort 更简洁:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- 匿名类是旧方法,冗长。
- Lambda 更现代且简洁。
5. 示例:按不同标准排序
按姓氏长度排序
Comparator<Person> byLastNameLength = (a, b) ->
Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);
先按年龄,再按名字(多级)
Comparator<Person> byAgeThenName = (a, b) -> {
int cmp = Integer.compare(a.getAge(), b.getAge());
if (cmp != 0) return cmp;
return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);
在搜索中使用比较器(示例)
比较器不仅用于排序,还可用于在已排序集合中搜索:
// people 必须按年龄排序!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
System.out.println("找到年龄为 22 的人:" + people.get(idx));
}
6. 最佳实践与使用 Comparator 的要点
不要违反契约
- 如果 compare(a, b) 返回 0,那么 compare(b, a) 也必须返回 0。
- 如果 compare(a, b) > 0,则 compare(b, a) 必须 < 0。
- 考虑可能出现的 null 值(见下文)。
别忘了 equals 和 hashCode
虽然比较器可以“自定义”比较对象,但对于 TreeSet 等结构或在 TreeMap 中查找键时,比较器的比较逻辑最好与 equals 保持一致。否则可能出现意外结果:两个不同对象按比较器相等,但按 equals 不相等。
考虑 null 的排序
如果字段可能为 null,请使用“现成”的辅助方法:
Comparator<Person> byLastNameNullSafe = Comparator.comparing(
Person::getLastName,
Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);
7. 实用细节
表格:Comparable 与 Comparator 对比
| Comparable | Comparator | |
|---|---|---|
| 在哪里实现? | 在类本身 | 在独立类/Lambda 中 |
| 方法 | |
|
| 方案数量 | 只有一个“自然” | 不限,按需定义 |
| 用法 | |
|
| 可用于第三方类? | 否 | 是 |
示例:降序排序
可以手动反转顺序:
Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);
或使用 reversed():
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());
8. 使用 Comparator 的常见错误
错误 1:违反比较契约。 如果你忽略了 compare(a, b) 与 compare(b, a) 的结果必须符号相反,或者返回随意的值(例如直接做差 a.getAge() - b.getAge(),这可能溢出),结果将不可预测。使用 Integer.compare 而不是相减——更安全。
错误 2:忽略 null 值。 如果用于比较的字段可能为 null,务必处理这种情况(例如通过 Comparator.nullsFirst/Comparator.nullsLast),否则很容易在意想不到的时候得到 NullPointerException。
错误 3:不稳定的排序标准。 如果比较器对同一对象对返回不同的值(例如使用随机数或高度可变的字段),排序行为可能会变得混乱。
错误 4:与 equals 不一致。 如果 compare(a, b) == 0,但 a.equals(b) 为 false,那么诸如 TreeSet、TreeMap 之类的集合可能不会按你的预期工作。最好让比较器的相等与 equals 的相等相一致。
错误 5:对第三方类不提供比较器就排序。 如果你尝试对没有实现 Comparable 的“第三方”类进行排序、且又未传入 Comparator,将得到编译错误。请显式传入比较器。
GO TO FULL VERSION