在 Java 中,自定义比较器(Comparator) 是实现对象自定义排序的核心工具,主要用于解决 Comparable
接口无法满足的灵活排序需求(如同一对象需多种排序规则、无法修改目标类源码等场景)。
一、核心概念:什么是 Comparator?
Comparator
是 Java 标准库 java.util
包下的函数式接口 (JDK 8+),核心作用是定义两个对象的比较规则,从而实现对象的排序。
- 与 Comparable 的区别 :
Comparable
是 "对象自身的排序能力"(让类实现Comparable
接口,重写compareTo
方法,固定一种排序规则);而Comparator
是 "外部的排序规则"(不修改目标类,通过外部类 / 匿名类 / Lambda 定义排序逻辑,支持多规则)。 - 核心方法 :只有一个抽象方法
int compare(T o1, T o2)
,返回值决定两个对象的顺序:- 返回 正数 :
o1
排在o2
后面(o1 > o2
); - 返回 负数 :
o1
排在o2
前面(o1 < o2
); - 返回 0 :
o1
和o2
排序位置相等(o1 == o2
,仅排序逻辑上相等,非对象相等)。
- 返回 正数 :
二、自定义比较器的 3 种实现方式
根据 Java 版本和代码风格,自定义比较器主要有 3 种实现方式,灵活性和简洁度依次提升:
1. 匿名内部类(JDK 7 及之前常用)
无需定义独立类,直接在使用处通过匿名内部类实现 Comparator
,适合简单排序场景。
示例 :对 String
按长度降序排序(长度相同则按字母升序)
import java.util.*;
public class ComparatorDemo {
public static void main(String[] args) {
List<String> list = Arrays.asList("apple", "banana", "cherry", "date");
// 匿名内部类实现 Comparator
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
// 1. 先按长度降序:s2长度 - s1长度(长度大的在前)
int lenDiff = s2.length() - s1.length();
if (lenDiff != 0) {
return lenDiff;
}
// 2. 长度相同则按字母升序(复用 String 自身的 compareTo 方法)
return s1.compareTo(s2);
}
});
System.out.println(list); // 输出:[banana, cherry, apple, date]
}
}
2. Lambda 表达式(JDK 8+ 推荐)
由于 Comparator
是函数式接口(仅一个抽象方法),可通过 Lambda 表达式简化代码,避免匿名内部类的冗余。
示例 :对 Integer
列表按 "奇数在前、偶数在后" 排序,奇数内部降序,偶数内部升序
import java.util.*;
public class ComparatorLambda {
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(3, 1, 4, 2, 5, 6);
// Lambda 表达式实现 Comparator
Collections.sort(nums, (a, b) -> {
// 规则1:奇数在前,偶数在后
if (a % 2 == 1 && b % 2 == 0) return -1; // a是奇数,b是偶数 → a在前
if (a % 2 == 0 && b % 2 == 1) return 1; // a是偶数,b是奇数 → b在前
// 规则2:同是奇数 → 降序(b - a)
if (a % 2 == 1) return b - a;
// 规则3:同是偶数 → 升序(a - b)
return a - b;
});
System.out.println(nums); // 输出:[5, 3, 1, 2, 4, 6]
}
}
3. 方法引用(JDK 8+,进一步简化)
若比较逻辑可复用现有方法(如类的静态方法、对象的实例方法),可通过方法引用直接引用该方法,无需手动写 Lambda 体。
Comparator
提供了多个静态方法(如 comparing
、thenComparing
),可快速构建比较器,配合方法引用更简洁。
常用静态方法:
方法 | 作用 | 示例(以 User 类为例) |
---|---|---|
comparing(Function keyExtractor) |
按 "提取的键" 升序排序 | Comparator.comparing(User::getAge) (按年龄升序) |
comparing(Function, Comparator) |
按 "提取的键"+ 自定义比较器排序 | comparing(User::getName, String.CASE_INSENSITIVE_ORDER) (按姓名忽略大小写排序) |
thenComparing(Comparator) |
主排序规则相等时,追加次要排序规则 | comparing(User::getAge).thenComparing(User::getName) (先按年龄升序,再按姓名升序) |
reverseOrder() |
返回自然排序的逆序比较器 | Comparator.reverseOrder() (对 Integer 降序) |
示例 :对 User
列表排序(先按年龄降序,再按姓名忽略大小写升序)
import java.util.*;
import java.util.Comparator;
// 定义 User 类
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Getter 方法(必须,用于方法引用提取键)
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() { return "User{name='" + name + "', age=" + age + "}"; }
}
public class ComparatorMethodRef {
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("Alice", 25),
new User("bob", 30),
new User("Bob", 25),
new User("Charlie", 28)
);
// 方法引用 + 静态方法构建比较器
Comparator<User> userComparator =
// 主规则:按年龄降序(reverseOrder 反转默认升序)
Comparator.comparing(User::getAge, Comparator.reverseOrder())
// 次要规则:年龄相等时,按姓名忽略大小写升序
.thenComparing(User::getName, String.CASE_INSENSITIVE_ORDER);
Collections.sort(users, userComparator);
System.out.println(users);
// 输出:
// [User{name='bob', age=30},
// User{name='Alice', age=25},
// User{name='Bob', age=25},
// User{name='Charlie', age=28}]
}
}
三、排序底层原理:为什么要遵守比较器契约?
Java 中 Collections.sort
和 Arrays.sort
(JDK 7+)默认使用 TimSort (一种归并排序和插入排序的混合算法),其排序正确性依赖于 compare
方法遵守比较器契约(即传递性、自反性、对称性)。
1. 必须遵守的 3 个契约
若 compare
方法违反以下规则,会抛出 IllegalArgumentException: Comparison method violates its general contract!
(就是你之前遇到的异常):
- 自反性 :
compare(a, a) == 0
(自己和自己比较,结果必须为 0); - 对称性 :若
compare(a, b) == x
,则compare(b, a) == -x
(a 和 b 比较的结果,与 b 和 a 比较的结果相反); - 传递性 :若
compare(a, b) ≤ 0
且compare(b, c) ≤ 0
,则compare(a, c) ≤ 0
(a ≤ b 且 b ≤ c → a ≤ c)。
2. 常见违反契约的场景(避坑!)
-
场景 1:忽略 null 值处理
若比较的对象可能为
null
,未处理会导致NullPointerException
,或违反对称性。
正确做法 :统一约定null
排在最前或最后,例如:Comparator<String> nullSafeComparator = (s1, s2) -> { if (s1 == null && s2 == null) return 0; if (s1 == null) return -1; // null 排在前面 if (s2 == null) return 1; return s1.compareTo(s2); };
-
场景 2:整数溢出
若通过
a - b
实现数字降序(如return b - a
),当b
是Integer.MAX_VALUE
、a
是Integer.MIN_VALUE
时,b - a
会溢出为负数,违反预期。
正确做法 :使用Integer.compare(a, b)
(JDK 提供的安全比较方法):// 错误:可能溢出 return b - a; // 正确:安全比较(降序) return Integer.compare(b, a);
-
场景 3:逻辑矛盾
例如比较两个字符串时,先按长度升序,再按字母降序,但代码中误写为 "长度升序 + 字母升序",导致传递性失效。
正确做法:明确排序优先级,每一步逻辑清晰,避免矛盾。
四、自定义比较器的应用场景
- 集合排序 :对
List
、TreeSet
(天然有序)、TreeMap
(键有序)进行自定义排序;- 例:
TreeSet<User>
用自定义比较器,实现 "按年龄降序存储";
- 例:
- 对象多规则排序 :同一对象需要多种排序方式(如
User
可按年龄排、按姓名排、按注册时间排); - 无法修改目标类 :目标类(如第三方库的类)未实现
Comparable
,或Comparable
的排序规则不符合需求; - 动态排序:排序规则需根据运行时条件切换(如用户选择 "按价格升序" 或 "按销量降序")。
五、关键注意事项总结
- 严格遵守比较器契约 :避免抛出
Comparison method violates its general contract
异常,尤其注意null
处理、整数溢出、传递性; - 优先使用 JDK 静态方法 :
Comparator.comparing
、thenComparing
等方法已封装安全逻辑,比手动写compare
更简洁、不易出错; - 函数式接口特性 :JDK 8+ 中,
Comparator
可作为方法参数传递,配合 Lambda / 方法引用大幅简化代码; - 区分 "排序相等" 与 "对象相等" :
compare(a, b) == 0
仅表示排序时位置相同,不代表a.equals(b)
;若用于TreeSet
/TreeMap
,会认为两者是 "相同元素"(不会重复存储),需注意逻辑一致性。