Comparable与Comparator的再学习与思考

2022-08-06 12:08:35

1. Comparable

1.1 源码

Comparable是java.lang包下的一个接口,其源码如下:

package java.lang;import java.util.*;publicinterfaceComparable<T>{publicintcompareTo(T o);}

可以看到,该接口下只有一个CompareTo方法,返回值为int。该比较方法主要用来比较对象o1与传入的对象o2的大小,即:

  • o1.compareTo(o2) > 0 即 o1 > o2;
  • o1.compareTo(o2) = 0 即 o1 = o2;
  • o1.compareTo(o2) < 0 即 o1 < o2;

1.2 小试牛刀

talk is cheap, show me the code. 撸一段代码悄悄先。

package com.example.demo.entity;import java.util.Objects;publicclassStudentimplementsComparable<Student>{privateint id;private String name;privateint age;publicStudent(int id, String name,int age){this.id= id;this.name= name;this.age= age;}publicintgetId(){return id;}publicvoidsetId(int id){this.id= id;}public StringgetName(){return name;}publicvoidsetName(String name){this.name= name;}publicintgetAge(){return age;}publicvoidsetAge(int age){this.age= age;}@OverridepublicintcompareTo(Student o){returngetId()- o.getId();}@Overridepublic StringtoString(){return"Student{"+"id="+ id+", name='"+ name+'\''+", age="+ age+'}';}}

这个pojo很简单,只有id, name和age三个字段,然后构造函数和toString方法,重要的是它实现了Comparable接口,并且实现了它的compareTo()方法, 根据id的大小进行比较。
写个测试类测试一下。

package com.example.demo.entity;import org.junit.Assert;import org.junit.Test;import java.util.*;publicclassStudentTest{@Testpublicvoidtest(){
        Student zhangsan=newStudent(12,"zhangsan",12);
        Student lisi=newStudent(13,"lisi",13);int i= zhangsan.compareTo(lisi);
        Assert.assertTrue(i<0);
        System.out.println(i);}}

通过测试用例,我们发现可以实现两个对象的比较。比较了大小之后,我们就可以实现更复杂的操作--排序

1.3 排序

排序的时候,我们需要借助java.util包底下的Collections类。(注意:这个是Collections,不是Collection类,虽然这两个长得很像,仅一个字母的差距,但这两个类之间的关系不输Java与JavaScript之间的差距)。

publicclassStudentTest{@Testpublicvoidtest(){
        Student zhangsan=newStudent(12,"zhangsan",12);
        Student lisi=newStudent(13,"lisi",13);
        Student wangwu=newStudent(15,"wangwu",15);
        List<Student> students=newArrayList<>(4);
        students.add(lisi);
        students.add(zhangsan);
        students.add(wangwu);
        
        Collections.sort(students);
        System.out.println(students);
        
        Collections.reverse(students);
        System.out.println(students);}}

输出的结果如下:

[Student{id=12, name='zhangsan', age=12}, Student{id=13, name='lisi', age=13}, Student{id=15, name='wangwu', age=15}][Student{id=15, name='wangwu', age=15}, Student{id=13, name='lisi', age=13}, Student{id=12, name='zhangsan', age=12}]

可以看到,只要将students传入到Collections.sort()方法中,就会将该list按升序排列,调用reverse就会将这个list再取逆序。

1.4 注意

查看API, 我们在Comparable这个类中看到:
This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class’s natural ordering, and the class’s compareTo method is referred to as its natural comparison method.
Lists (and arrays) of objects that implement this interface can be sorted automatically by Collections.sort (and Arrays.sort). Objects that implement this interface can be used as keys in a sorted map or as elements in a sorted set, without the need to specify a comparator.

The natural ordering for a class C is said to be consistent with equals if and only if e1.compareTo(e2) == 0 has the same boolean value as e1.equals(e2) for every e1 and e2 of class C. Note that null is not an instance of any class, and e.compareTo(null) should throw a NullPointerException even though e.equals(null) returns false.

It is strongly recommended (though not required) that natural orderings be consistent with equals. This is so because sorted sets (and sorted maps) without explicit comparators behave “strangely” when they are used with elements (or keys) whose natural ordering is inconsistent with equals. In particular, such a sorted set (or sorted map) violates the general contract for set (or map), which is defined in terms of the equals method.

For example, if one adds two keys a and b such that (!a.equals(b) && a.compareTo(b) == 0) to a sorted set that does not use an explicit comparator, the second add operation returns false (and the size of the sorted set does not increase) because a and b are equivalent from the sorted set’s perspective.

Virtually all Java core classes that implement Comparable have natural orderings that are consistent with equals. One exception is java.math.BigDecimal, whose natural ordering equates BigDecimal objects with equal values and different precisions (such as 4.0 and 4.00).

For the mathematically inclined, the relation that defines the natural ordering on a given class C is:

   {(x, y) such that x.compareTo(y) <= 0}.

The quotient for this total order is:
{(x, y) such that x.compareTo(y) == 0}.

It follows immediately from the contract for compareTo that the quotient is an equivalence relation on C, and that the natural ordering is a total order on C. When we say that a class’s natural ordering is consistent with equals, we mean that the quotient for the natural ordering is the equivalence relation defined by the class’s equals(Object) method:
{(x, y) such that x.equals(y)}.
This interface is a member of the Java Collections Framework.

从中我们可以看到:建议我们将compareTo的结果与equals的结果设置为一致的。即 如果e1.compareTo(e2) == 0 , 那么最好e1.equals(e2)。
如果不一致会怎么样呢?即 (!e1.equals(e2) && e1.compareTo(e2) == 0),在不使用具体的comparator时,sorted set的add操作,第一次操作成功,但是第二次操作会失败,返回false。是这样吗?上代码!

package com.example.demo.entity;import org.junit.Assert;import org.junit.Test;import java.util.*;publicclassStudentTest{@Testpublicvoidtest(){
        Student zhangsan1=newStudent(12,"zhangsan",12);
        Student zhangsan2=newStudent(12,"zhangsan",12);
        
        TreeSet<Student> set=newTreeSet<>();
        System.out.println(set.add(zhangsan1));
        System.out.println(set.size());

        System.out.println(set.add(zhangsan2));
        System.out.println(set.size());}}

输出的结果是

true
1false
1

我们可以清楚的看到,第一次成功了,第二次失败了,set的size还是1。这是为啥?凭啥zhangsan1可以,zhangsan2就不可以?你歧视我法外狂徒张三大哥?
哎,还真就是歧视了!我们跟源码可以发现,TreeSet的add操作是

publicbooleanadd(E e){return m.put(e, PRESENT)==null;}

再跟,发现进入到了TreeMap中的put方法

public Vput(K key, V value){
        Entry<K,V> t= root;//zhangsan1已经在了,那就不为空if(t== null){compare(key, key);// type (and possibly null) check

            root=newEntry<>(key, value, null);
            size=1;
            modCount++;return null;}int cmp;
        Entry<K,V> parent;// split comparator and comparable paths
        Comparator<?super K> cpr= comparator;//又没有指定comparator,这里也是空,进入elseif(cpr!= null){do{
                parent= t;
                cmp= cpr.compare(key, t.key);if(cmp<0)
                    t= t.left;elseif(cmp>0)
                    t= t.right;elsereturn t.setValue(value);}while(t!= null);}else{//聚光灯在哪里?快看这里if(key== null)thrownewNullPointerException();@SuppressWarnings("unchecked")
                Comparable<?super K> k=(Comparable<?super K>) key;//本身就是已经实现了comparable接口的do{
                parent= t;
                cmp= k.compareTo(t.key);//重头戏在这里,我们得zhansgan1与zhangsan2的compareTo方法的结果是0,它俩是相等的if(cmp<0)
                    t= t.left;elseif(cmp>0)
                    t= t.right;elsereturn t.setValue(value);//这里执行setValue操作}while(t!= null);}
        Entry<K,V> e=newEntry<>(key, value, parent);if(cmp<0)
            parent.left= e;else
            parent.right= e;fixAfterInsertion(e);
        size++;
        modCount++;return null;}

当执行return t.setValue(value)操作后

public VsetValue(V value){
            V oldValue=this.value;this.value= value;return oldValue;}

返回了oldValue,这return m.put(e, PRESENT)==null; 执行的就是false了。
仔细想来,TreeSet是红黑树,既然是红黑树,那就是为了排序嘛!搞一个(!e1.equals(e2) && e1.compareTo(e2) == 0)就很别扭嘛!这种既相等又不相等,要逼死强迫症吗!
但是,别扭归别扭,只是建议,不是强制!但是强烈建议。

2.Comparator

对于比较,上面的类我们实现了Comparable接口,但是如果我们没有办法实现Comparable接口,又想要进行比较操作该如何实现呢?这就需要用到我们的另一个类-Comparator。

2.1 源码

Comparator是java.util包下的一个接口,在jdk 1.8之前,方法还比较少,但是在1.8当中加入了很多方法。这里暂时只关注一下compar(T,T)方法。

2.2 小试牛刀

package com.example.demo.entity;import java.util.Objects;publicclassStudent{privateint id;private String name;privateint age;publicStudent(int id, String name,int age){this.id= id;this.name= name;this.age= age;}publicintgetId(){return id;}publicvoidsetId(int id){this.id= id;}public StringgetName(){return name;}publicvoidsetName(String name){this.name= name;}publicintgetAge(){return age;}publicvoidsetAge(int age){this.age= age;}@Overridepublic StringtoString(){return"Student{"+"id="+ id+", name='"+ name+'\''+", age="+ age+'}';}@Overridepublicbooleanequals(Object o){if(this== o)returntrue;if(o== null||getClass()!= o.getClass())returnfalse;
        Student student=(Student) o;return id== student.id&&
                age== student.age&&
                Objects.equals(name, student.name);}@OverridepublicinthashCode(){return Objects.hash(id, name, age);}}

简单粗暴上代码!还是搞个UT先。

package com.example.demo.entity;import org.junit.Assert;import org.junit.Test;import java.util.*;publicclassStudentTest{@Testpublicvoidtest(){
        Student zhangsan1=newStudent(12,"zhangsan",12);
        Student zhangsan2=newStudent(12,"zhangsan",12);
        Student lisi=newStudent(13,"lisi",13);
        Student wangwu=newStudent(15,"wangwu",15);
        List<Student> students=newArrayList<>(4);
        students.add(lisi);
        students.add(zhangsan1);
        students.add(zhangsan2);
        students.add(wangwu);

        Comparator<Student> stuComparator=newComparator<Student>(){@Overridepublicintcompare(Student o1, Student o2){return o1.getId()-o2.getId();}};

        Collections.sort(students, stuComparator);
        System.out.println(students);

        Collections.reverse(students);
        System.out.println(students);}}

这样的输出结果是

[Student{id=12, name='zhangsan', age=12}, Student{id=12, name='zhangsan', age=12}, Student{id=13, name='lisi', age=13}, Student{id=15, name='wangwu', age=15}][Student{id=15, name='wangwu', age=15}, Student{id=13, name='lisi', age=13}, Student{id=12, name='zhangsan', age=12}, Student{id=12, name='zhangsan', age=12}]

想着Java8 都有lambda表达式了,那还不动动手?

package com.example.demo.entity;import org.junit.Assert;import org.junit.Test;import java.util.*;publicclassStudentTest{@Testpublicvoidtest(){
        Student zhangsan1=newStudent(12,"zhangsan",12);
        Student zhangsan2=newStudent(12,"zhangsan",12);
        Student lisi=newStudent(13,"lisi",13);
        Student wangwu=newStudent(15,"wangwu",15);
        List<Student> students=newArrayList<>(4);
        students.add(lisi);
        students.add(zhangsan1);
        students.add(zhangsan2);
        students.add(wangwu);//Comparator<Student> stuComparator = (o1, o2) -> o1.getId() -o2.getId();

        Collections.sort(students,(o1, o2)-> o1.getId()-o2.getId());
        System.out.println(students);

        Collections.reverse(students);
        System.out.println(students);}}

输出的结果必定是一样的。

3. 总结

  • 这两个类所在的包不同,Comparable是属于java.lang, 而comparator是属于java.util
  • Comparable提供的是compareTo(T), 而Comparator提供的是compare(T, T)
  • 适用场景不同,对于自己写的pojo,可以实现Comparable接口,但是无法更改的类,可以用comparator进行排序。
  • 使
  • 作者:圆师傅
  • 原文链接:https://blog.csdn.net/Apple_wolf/article/details/113179678
    更新时间:2022-08-06 12:08:35