zoukankan      html  css  js  c++  java
  • Java Comparable 和 Comparator 接口详解


    本文基于 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());
      

  • 相关阅读:
    使用RestTemplate发送post请求,请求头中封装参数
    LocalDateTime的一些用法
    java实现遍历文件目录,根据文件最后的修改时间排序,并将文件全路径存入List集合
    【Quartz】Quartz存储与持久化-基于quartz.properties的配置
    《分布式任务调度平台XXL-JOB》
    SpringBoot集成Quartz实现定时器
    springboot整合Quartz实现动态配置定时任务
    Java读取文件创建时间和最后修改时间
    杂七杂八快捷键🍿
    git命令笔记
  • 原文地址:https://www.cnblogs.com/Yee-Q/p/13729929.html
Copyright © 2011-2022 走看看