Java Comparable 和 Comparator 接口详解

2年前 (2022) 程序员胖胖胖虎阿
271 0 0

本文基于 JDK8 分析

Comparable

Comparable 接口位于 java.lang 包下,Comparable 接口下有一个 compareTo 方法,称为自然比较方法。一个类只要实现了这个接口,意味着该类支持自然排序

所谓自然排序,就是按默认规则组成的排序,例如 1234 就是自然排序,因为 2 就是比 1 大,这是默认规定的。类比到 Comparable,我们在 compareTo 中定义自己需要的默认比较规则,以后如果用到 Collections.sort 和 Arrays.sort 方法排序,或者是作为 SortedSet、SortedMap 等组件的元素,就可以按照我们想要的规则排序了

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

Comparable 的用法

Comparable 接口在 JDK8 中的源码

// T 是可比较的类型
public interface Comparable<T> {
    public int compareTo(T o);
}

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

  • 返回 0 表示当前对象与目标对象相等
  • 返回正数表示当前对象比目标对象大
  • 返回负数表示当前对象比目标对象小
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();
    }
}

compareTo 和 equals

强烈建议自然排序和 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

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

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

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

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) -> {...});

Comparator 中常用的默认方法

相比于 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());
    

版权声明:程序员胖胖胖虎阿 发表于 2022年9月26日 上午11:00。
转载请注明:Java Comparable 和 Comparator 接口详解 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...