前言

Java 提供了两种对象比较的方式:

  1. 定义可排序类,实现 Comparable 接口。
  2. 定义比较器,实现 Comparator 接口。

下面介绍两种方式的使用区别,并给出案例。

版本约定

Comparable 接口位于 java.lang 包下,Comparable 接口下有一个 compareTo 方法,称为自然比较方法。

一个类只要实现了这个接口,意味着该类支持自然排序,以后如果用到 Collections.sort 和 Arrays.sort 方法排序,或者是作为 SortedSet、SortedMap 等组件的元素,就可以按照默认比较规则排序了,不需要额外指定比较器。

比较的对象不应该出现 null,因为 null 不属于任何类的实例。如果出现了 e.compareTo(null) 这种情况,应该抛出 NullPointerException。

Comparable 的用法

需要比较的类只需实现 Comparable 接口,在 compareTo 方法中定义自己的比较规则。

  • 返回 0 表示当前对象与目标对象相等。
  • 返回正数表示当前对象比目标对象大。
  • 返回负数表示当前对象比目标对象小。

    1. public class Employee implements Comparable<Employee> {
    2. private String name;
    3. private double salary;
    4. // 构造方法,get方法,set方法
    5. @Override
    6. public int compareTo(Employee other) {
    7. return Double.compare(salary, other.salary);
    8. }
    9. }
    1. public static void main(String[] args) {
    2. // 对象比较
    3. Employee e1 = new Employee("Harry Hacker", 35000);
    4. Employee e2 = new Employee("Carl Cracker", 75000);
    5. System.out.println("e1.compareTo(e2): " + e1.compareTo(e2));
    6. // 数组对象比较
    7. Employee e3 = new Employee("Tony Tester", 38000);
    8. Employee[] array = new Employee[3];
    9. array[0] = e1;
    10. array[1] = e2;
    11. array[2] = e3;
    12. Arrays.sort(array);
    13. System.out.println("array:" + Arrays.toString(array));
    14. // 集合比较
    15. List<Employee> list = Lists.newArrayList(e1, e2, e3);
    16. Collections.sort(list);
    17. System.out.println("list:" + list);
    18. }

    运行程序,输出:

    1. e1.compareTo(e2): -1
    2. array:[Employee{name='Harry Hacker', salary=35000.0}, Employee{name='Tony Tester', salary=38000.0}, Employee{name='Carl Cracker', salary=75000.0}]
    3. 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),如果 Manage 继承 Employee,覆盖了 compareTo 方法,就必须要有经理和雇员进行比较的思想准备,不能简单的将雇员转换成经理。

  1. class Manager extends Employee {
  2. public int compareTo(Employee other) {
  3. Manager otherManager = (Manager) other;
  4. ......
  5. }
  6. }

这不符合“反对称”的规则,如果 x 是一个 Employee 对象,y 是一个 Manager 对象,调用 x.compareTo(y) 不会抛出异常,它只是将 x 和 y 都作为雇员进行比较。但是反过来,y.compareTo(x) 将会抛出一个 ClassCastException。

推荐的处理方式和 equals 一样,分为两种不同的情况。

  1. 如果子类之间的比较含义不一样,那么各自类实现各自的比较规则,每个 compareTo 方法都应该在开始时进行如下检查:if (getClass() != other.getClass()) throw new ClassCastException;
  2. 如果存在这样一种通用算法,它能够对两种不同的子类对象进行比较,则应该在超类中提供一个 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。

  1. // TreeSet 中的 add 方法,基于 TreeMap 的 put 方法实现
  2. public boolean add(E e) {
  3. return m.put(e, PRESENT) == null;
  4. }
  5. // TreeMap 中的 put 方法,这里我们只关注被注释的那一段代码即可
  6. public V put(K key, V value) {
  7. Entry<K,V> t = root;
  8. if (t == null) {
  9. compare(key, key);
  10. root = new Entry<>(key, value, null);
  11. size = 1;
  12. modCount++;
  13. return null;
  14. }
  15. int cmp;
  16. Entry<K,V> parent;
  17. Comparator<? super K> cpr = comparator;
  18. if (cpr != null) {
  19. do {
  20. parent = t;
  21. cmp = cpr.compare(key, t.key);
  22. if (cmp < 0)
  23. t = t.left;
  24. else if (cmp > 0)
  25. t = t.right;
  26. else
  27. return t.setValue(value);
  28. } while (t != null);
  29. }
  30. // 这里使用 compareTo 对元素作自然排序
  31. else {
  32. if (key == null)
  33. throw new NullPointerException();
  34. @SuppressWarnings("unchecked")
  35. Comparable<? super K> k = (Comparable<? super K>) key;
  36. do {
  37. parent = t;
  38. cmp = k.compareTo(t.key);
  39. if (cmp < 0)
  40. t = t.left;
  41. else if (cmp > 0)
  42. t = t.right;
  43. else
  44. // 就是在这里遇到相等的元素(根据比较器比较)
  45. return t.setValue(value);
  46. } while (t != null);
  47. }
  48. Entry<K,V> e = new Entry<>(key, value, parent);
  49. if (cmp < 0)
  50. parent.left = e;
  51. else
  52. parent.right = e;
  53. fixAfterInsertion(e);
  54. size++;
  55. modCount++;
  56. return null;
  57. }

Comparator

Comparator 位于 java.util 包下,也是用来排序的。与 Comparable 不同的是,Comparable 表示该类“可以支持排序”,自身提供了排序方法;而 Comparator 则是一个“比较器”,这个比较器需要实现 Comparator 接口,可以通过这个比较器来对类排序,类本身不需要支持排序。

当需要对数组或者集合作排序操作,如 Collections.sort 或是 Arrays.sort 时,把比较器作为参数传进去即可。也可以使用 Comparator 来控制某些集合(TreeSet 或 TreeMap),如果集合要被序列化,Comparator 比较器也必须实现序列化接口。

所以说,Comparator 和 Comparable 本质上没有什么区别,Comparable 要注意的点在 Comparator 中亦是如此。

Comparator 的用法

实现 Comparator 接口,在 compare 方法中定义自己的比较规则。

  1. public class EmployeeComparator implements Comparator<Employee> {
  2. @Override
  3. public int compare(Employee o1, Employee o2) {
  4. return Double.compare(o1.getSalary(), o2.getSalary());
  5. }
  6. }
  1. public static void main(String[] args) {
  2. // 对象比较
  3. Employee e1 = new Employee("Harry Hacker", 35000);
  4. Employee e2 = new Employee("Carl Cracker", 75000);
  5. int result = new EmployeeComparator().compare(e1, e2);
  6. System.out.println("compare(e1, e2): " + result);
  7. // 数组对象比较
  8. Employee e3 = new Employee("Tony Tester", 38000);
  9. Employee[] array = new Employee[3];
  10. array[0] = e1;
  11. array[1] = e2;
  12. array[2] = e3;
  13. Arrays.sort(array, new EmployeeComparator());
  14. System.out.println("array:" + Arrays.toString(array));
  15. // 集合比较
  16. List<Employee> list = Lists.newArrayList(e1, e2, e3);
  17. Collections.sort(list, new EmployeeComparator());
  18. System.out.println("list:" + list);
  19. // 使用内部类定义Comparator
  20. Collections.sort(list, new Comparator<Employee>() {
  21. @Override
  22. public int compare(Employee o1, Employee o2) {
  23. return Double.compare(o1.getSalary(), o2.getSalary());
  24. }
  25. });
  26. // 使用lambda表达式定义Comparator
  27. Collections.sort(list, (o1, o2) -> {
  28. return Double.compare(o1.getSalary(), o2.getSalary());
  29. });
  30. }

运行程序,输出:

  1. compare(e1, e2): -1
  2. array:[Employee{name='Harry Hacker', salary=35000.0}, Employee{name='Tony Tester', salary=38000.0}, Employee{name='Carl Cracker', salary=75000.0}]
  3. 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 提供了一些默认方法和静态方法,功能更加强大。

  1. reversed

返回一个比较器,是原比较器的逆序(没有实现则是自然排序),底层使用 Collections 的 reverseOrder 方法实现。

  1. comparing

返回一个比较器,比较规则由传入的参数制定,该方法有两个重载方法。

  1. // 参数为要比较的元素类型,默认按自然排序比较
  2. public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor)
  3. // 第一个参数为要比较的元素类型,第二个参数为比较规则
  4. public static <T, U> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator)

用法如下:

  1. Collections.sort(list, Comparator.comparing(User::getAge));
  2. Collections.sort(list, Comparator.comparing(Student::getLikeGame, Comparator.reverseOrder()));
  1. thenComparing

多条件排序的方法,当我们排序的条件不止一个的时候可以使用该方法。比如说我们对 Employee 先按照 salary 字段排序,再按照 id 排序,就可以使用 thenComparing 方法。

Collections.sort(list, comparator.thenComparing(o -> o.getId()));

thenComparing 有很多重载方法,功能都一样的,但有一点要注意:传进去的类型都是按照自然排序,id 是一个整数,规则就是 1234 从小到大排序。如果你传进去的是一个对象,而你希望能自定义比较规则,那么这个对象必须实现 Comparable 接口。

  1. nullsFirst 和 nullsLast

这两个方法的意思是,如果排序的字段为 null 的情况下,这条记录该如何处理。nullsFirst 是将这条记录排在最前面,而 nullsLast 是将这条记录排序在最后面。如果多个 key 都为 null 的话,将无法保证这几个对象的排序。

Comparator<User> comparator = Comparator.comparing(User::getAge, Comparator.nullsLast(Comparator.reverseOrder()));

  1. reverseOrder 和 naturalOrder

naturalOrder 返回自然排序的比较器,reverseOrder 则是逆序。比较的对象,必须实现 Comparable 接口。

参考

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/sn5bqa 来源:殷建卫 - 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。