前言
Java 提供了两种对象比较的方式:
- 定义可排序类,实现 Comparable 接口。
- 定义比较器,实现 Comparator 接口。
版本约定
- JDK Version:11.0.12
- Java SE API Documentation:https://docs.oracle.com/en/java/javase/11/docs/api/index.html
正文
Comparable
Comparable 接口位于 java.lang 包下,Comparable 接口下有一个 compareTo 方法,称为自然比较方法。
一个类只要实现了这个接口,意味着该类支持自然排序,以后如果用到 Collections.sort 和 Arrays.sort 方法排序,或者是作为 SortedSet、SortedMap 等组件的元素,就可以按照默认比较规则排序了,不需要额外指定比较器。
比较的对象不应该出现 null,因为 null 不属于任何类的实例。如果出现了 e.compareTo(null) 这种情况,应该抛出 NullPointerException。
Comparable 的用法
需要比较的类只需实现 Comparable 接口,在 compareTo 方法中定义自己的比较规则。
- 返回 0 表示当前对象与目标对象相等。
- 返回正数表示当前对象比目标对象大。
返回负数表示当前对象比目标对象小。
public class Employee implements Comparable<Employee> {
private String name;
private double salary;
// 构造方法,get方法,set方法
@Override
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
}
public static void main(String[] args) {
// 对象比较
Employee e1 = new Employee("Harry Hacker", 35000);
Employee e2 = new Employee("Carl Cracker", 75000);
System.out.println("e1.compareTo(e2): " + e1.compareTo(e2));
// 数组对象比较
Employee e3 = new Employee("Tony Tester", 38000);
Employee[] array = new Employee[3];
array[0] = e1;
array[1] = e2;
array[2] = e3;
Arrays.sort(array);
System.out.println("array:" + Arrays.toString(array));
// 集合比较
List<Employee> list = Lists.newArrayList(e1, e2, e3);
Collections.sort(list);
System.out.println("list:" + list);
}
运行程序,输出:
e1.compareTo(e2): -1
array:[Employee{name='Harry Hacker', salary=35000.0}, Employee{name='Tony Tester', salary=38000.0}, Employee{name='Carl Cracker', salary=75000.0}]
list:[Employee{name='Harry Hacker', salary=35000.0}, Employee{name='Tony Tester', salary=38000.0}, Employee{name='Carl Cracker', salary=75000.0}]
Comparable 实现要满足对称性
Java 语言规范规定:对于任意的 x 和 y,实现必须能够保证 sgn(x.compareTo(y)) = -sgn(y.compareTo(x))。
这里的“sgn”是一个数值的符号:如果 n 是负值,sgn(n) 等于 -1;如果 n 是 0,sgn(n) 等于 0;如果 n 是正值,sgn(n) 等于 1。
也就是说,如果 x.compareTo(y) 抛出一个异常,那么 y.compareTo(x) 也应该抛出一个异常。如果调换 compareTo 的参数,结果的符号也应该调换。
与 equals 方法一样,在继承过程中有可能会出现问题。
比如,Employee 实现了 Comparable 接口(Comparable
class Manager extends Employee {
public int compareTo(Employee other) {
Manager otherManager = (Manager) other;
......
}
}
这不符合“反对称”的规则,如果 x 是一个 Employee 对象,y 是一个 Manager 对象,调用 x.compareTo(y) 不会抛出异常,它只是将 x 和 y 都作为雇员进行比较。但是反过来,y.compareTo(x) 将会抛出一个 ClassCastException。
推荐的处理方式和 equals 一样,分为两种不同的情况。
- 如果子类之间的比较含义不一样,那么各自类实现各自的比较规则,每个 compareTo 方法都应该在开始时进行如下检查:
if (getClass() != other.getClass()) throw new ClassCastException;
。 - 如果存在这样一种通用算法,它能够对两种不同的子类对象进行比较,则应该在超类中提供一个 compareTo 方法,并将这个方法声明为 final。
例如,假设不管薪水的多少都想让经理大于雇员,这样子类该怎么实现呢?
如果一定要按照职务排序的话,可以在 Employee 类中提供一个 rank 方法。每个子类覆盖 rank,并实现一个考虑 rank 值的 compareTo 方法。
compareTo 和 equals 的结果要保持一致
建议 compareTo 和 equals 方法的执行结果要保持一致。就是如果 x.compareTo(y) 的执行结果等于 0,那么 x.equals(y) 的执行结果要等于 true。
例如,TreeSet 是一个 Set 集合,通过 hashCode 和 equals 方法来判断元素是否唯一,同时还会根据 Comparator 或是 Comparable 接口对元素进行排序。假如出现了 equals 和 compareTo 的行为不一致,就会出现比较诡异的情况,JDK 官方文档有对该情况的说明:
如果将两个键 a 和 b 添加到没有使用显式比较器的有序集合中,使得 (!a.equals(b) && a.compareTo(b) == 0) 成立,那么第二个 add 操作(添加 b)将返回 false(有序集合的大小没有增加),因为从有序集合的角度来看,a 和 b 是相等的。
明明 equals 已经判断该元素不重复,但还是拒绝了添加操作,因为 compaTo 认为这两个元素是相等的,这明显不是我们想要的结果。正确的分工是,equals 负责判断元素唯一性,compareTo 负责元素的排序,两者互不干扰。
下面以 TreeSet 为例,TreeSet 的 add 方法基于 TreeMap 的 put 方法实现,TreeMap 的结构是一颗红黑树,会根据默认比较器一直向下迭代,直到某个节点的左子树或右子树为 null,并将元素插入到该节点的左子树或右子树,并对整棵树重写进行颜色绘制。如果发现树中某个节点的值和待插入元素元素一致,则覆盖并返回旧值。回到 TreeSet 的 add 方法,put 方法的返回值不为 null,自然 add 方法的返回值就是 false。
// TreeSet 中的 add 方法,基于 TreeMap 的 put 方法实现
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
// TreeMap 中的 put 方法,这里我们只关注被注释的那一段代码即可
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key);
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 这里使用 compareTo 对元素作自然排序
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
// 就是在这里遇到相等的元素(根据比较器比较)
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
Comparator
Comparator 位于 java.util 包下,也是用来排序的。与 Comparable 不同的是,Comparable 表示该类“可以支持排序”,自身提供了排序方法;而 Comparator 则是一个“比较器”,这个比较器需要实现 Comparator 接口,可以通过这个比较器来对类排序,类本身不需要支持排序。
当需要对数组或者集合作排序操作,如 Collections.sort 或是 Arrays.sort 时,把比较器作为参数传进去即可。也可以使用 Comparator 来控制某些集合(TreeSet 或 TreeMap),如果集合要被序列化,Comparator 比较器也必须实现序列化接口。
所以说,Comparator 和 Comparable 本质上没有什么区别,Comparable 要注意的点在 Comparator 中亦是如此。
Comparator 的用法
实现 Comparator 接口,在 compare 方法中定义自己的比较规则。
public class EmployeeComparator implements Comparator<Employee> {
@Override
public int compare(Employee o1, Employee o2) {
return Double.compare(o1.getSalary(), o2.getSalary());
}
}
public static void main(String[] args) {
// 对象比较
Employee e1 = new Employee("Harry Hacker", 35000);
Employee e2 = new Employee("Carl Cracker", 75000);
int result = new EmployeeComparator().compare(e1, e2);
System.out.println("compare(e1, e2): " + result);
// 数组对象比较
Employee e3 = new Employee("Tony Tester", 38000);
Employee[] array = new Employee[3];
array[0] = e1;
array[1] = e2;
array[2] = e3;
Arrays.sort(array, new EmployeeComparator());
System.out.println("array:" + Arrays.toString(array));
// 集合比较
List<Employee> list = Lists.newArrayList(e1, e2, e3);
Collections.sort(list, new EmployeeComparator());
System.out.println("list:" + list);
// 使用内部类定义Comparator
Collections.sort(list, new Comparator<Employee>() {
@Override
public int compare(Employee o1, Employee o2) {
return Double.compare(o1.getSalary(), o2.getSalary());
}
});
// 使用lambda表达式定义Comparator
Collections.sort(list, (o1, o2) -> {
return Double.compare(o1.getSalary(), o2.getSalary());
});
}
运行程序,输出:
compare(e1, e2): -1
array:[Employee{name='Harry Hacker', salary=35000.0}, Employee{name='Tony Tester', salary=38000.0}, Employee{name='Carl Cracker', salary=75000.0}]
list:[Employee{name='Harry Hacker', salary=35000.0}, Employee{name='Tony Tester', salary=38000.0}, Employee{name='Carl Cracker', salary=75000.0}]
Comparator 中常用的默认方法
相比于 Comparable,Comparator 提供了一些默认方法和静态方法,功能更加强大。
- reversed
返回一个比较器,是原比较器的逆序(没有实现则是自然排序),底层使用 Collections 的 reverseOrder 方法实现。
- comparing
返回一个比较器,比较规则由传入的参数制定,该方法有两个重载方法。
// 参数为要比较的元素类型,默认按自然排序比较
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor)
// 第一个参数为要比较的元素类型,第二个参数为比较规则
public static <T, U> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator)
用法如下:
Collections.sort(list, Comparator.comparing(User::getAge));
Collections.sort(list, Comparator.comparing(Student::getLikeGame, Comparator.reverseOrder()));
- thenComparing
多条件排序的方法,当我们排序的条件不止一个的时候可以使用该方法。比如说我们对 Employee 先按照 salary 字段排序,再按照 id 排序,就可以使用 thenComparing 方法。
Collections.sort(list, comparator.thenComparing(o -> o.getId()));
thenComparing 有很多重载方法,功能都一样的,但有一点要注意:传进去的类型都是按照自然排序,id 是一个整数,规则就是 1234 从小到大排序。如果你传进去的是一个对象,而你希望能自定义比较规则,那么这个对象必须实现 Comparable 接口。
- nullsFirst 和 nullsLast
这两个方法的意思是,如果排序的字段为 null 的情况下,这条记录该如何处理。nullsFirst 是将这条记录排在最前面,而 nullsLast 是将这条记录排序在最后面。如果多个 key 都为 null 的话,将无法保证这几个对象的排序。
Comparator<User> comparator = Comparator.comparing(User::getAge, Comparator.nullsLast(Comparator.reverseOrder()));
- reverseOrder 和 naturalOrder
naturalOrder 返回自然排序的比较器,reverseOrder 则是逆序。比较的对象,必须实现 Comparable 接口。
参考
- Java 核心技术 卷1 基础知识 第10版
- Java Comparable 和 Comparator 接口详解
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/sn5bqa 来源:殷建卫 - 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。