本文基于 JDK8 分析
Comparable 接口位于 java.lang 包下,Comparable 接口下有一个 compareTo 方法,称为自然比较方法。一个类只要实现了这个接口,意味着该类支持自然排序
所谓自然排序,就是按默认规则组成的排序,例如 1234 就是自然排序,因为 2 就是比 1 大,这是默认规定的。类比到 Comparable,我们在 compareTo 中定义自己需要的默认比较规则,以后如果用到 Collections.sort 和 Arrays.sort 方法排序,或者是作为 SortedSet、SortedMap 等组件的元素,就可以按照我们想要的规则排序了
比较的对象不应该出现 null,因为 null 不属于任何类的实例。如果出现了 e.compareTo(null) 这种情况,应该抛出 NullPointerException
Comparable 接口在 JDK8 中的源码
// T 是可比较的类型
public interface Comparable<T> {
public int compareTo(T o);
}
需要比较的类只需实现 Comparable 接口即可,在 compareTo 中定义自己的比较规则
public class User implements Comparable<User>{
private Integer id;
private Integer age;
// 构造方法和 set/get 方法省略 ...
// 第一种实现方式
public int compareTo(User o) {
// 根据用户的年龄比较,参数 o 为目标比较对象
if(this.age > o.getAge()) {
// 当前对象比目标对象大,则返回 1
return 1;
}else if(this.age < o.getAge()) {
// 当前对象比目标对象小,则返回 -1
return -1;
}else{
// 若是两个对象相等,则返回 0
return 0;
}
}
// 第二种实现方式
public int compareTo(User o) {
return this.age - o.getAge();
}
}
强烈建议自然排序和 equals 的顺序保持一致(就是两个对象调用 compareTo 方法和调用 equals 方法返回的布尔值应该一样)
这个建议在需要同时保持元素有序和唯一的集合中尤其重要。例如 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 位于 java.util 包下,也是用来排序的。与 Comparable 不同的是,Comparable 表示该类“可以支持排序”,自身提供了排序方法;而 Comparator 则是一个“比较器”,这个比较器需要实现 Comparator 接口,可以通过这个比较器来对类排序,类本身不需要任何操作
当需要作排序操作如 Collections.sort 或是 Arrays.sort 时,把比较器作为参数传进去即可。也可以使用 Comparator 来控制某些集合(TreeSet 或 TreeMap),如果集合要被序列化,Comparator 比较器也必须实现序列化接口
所以说,Comparator 和 Comparable 本质上没有什么区别,Comparable 要注意的点在 Comparator 中亦是如此
自定义一个 User 实体类
public class User {
private Integer id;
private Integer age;
// 构造方法和 set/get 方法省略 ...
}
自定义比较器
class AgeComparator implements Comparator<User> {
@Override
public int compare(User u1, User u2) {
if (u1.getAge() > u2.getAge()) {
return 1;
} else if (u1.getAge() < u2.getAge()) {
return -1;
} else {
return 0;
}
}
}
要使用比较器,只需要直接创建即可。也可以使用匿名内部类或者 lambda 表达式
// 已经定义了比较器,可直接使用
Collections.sort(list, new AgeComparator());
// 使用匿名内部类
Collections.sort(list, new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
...
}
});
// 使用 lambda 表达式
Collections.sort(list, (u1, u2) -> {...});
相比于 Comparable,Comparator 提供了更多默认方法和静态方法,功能更加强大
reversed
返回一个比较器,是原比较器的逆序(没有实现则是自然排序),底层使用 Collections 的 reverseOrder 方法实现
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
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
多条件排序的方法,当我们排序的条件不止一个的时候可以使用该方法。比如说我们对 User 先按照 age 字段排序,再按照 id 排序,就可以使用 thenComparing 方法
Collections.sort(list, comparator.thenComparing(x -> x.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
返回自然排序的比较器,reverseOrder 则是逆序。同样的,对于自然排序,如果希望自定义规则,必须实现 Comparable 接口
Collections.sort(list, Comparator.reverseOrder());