【Java基础】Comparable和Comparator两种比较器使用

自然排序是什么: 如果是基本类型,那就是自然顺序,由小到大。如果是对象,那就是比较哈希值,由小到大。

一.Comparator和Comparable区别

在这里插入图片描述

  1. ComparableComparator都是用来实现集合中元素的比较、排序的两种比较器接口,这两种比较器很相似,只是 Comparable 是在集合内部定义的方法实现的排序,可称之为内比较器,而Comparator 是在集合外部实现的排序,可称之为外比较器。所以,如果想实现排序,就需要在集合外定义 Comparator 接口的方法或在集合内实现 Comparable 接口的方法

  2. 实际上也就是根据类中的某个字段来比较这两个类的大小。说是比较,其实更多的是排序,利用Collections.sort()Arrays.sort()这两个工具类进行排序。

  3. Comparable & Comparator
    接口都可以用来实现集合中元素的比较、排序,Comparator位于包java.util.Comparator下,而Comparable位于包java.lang.Comparable

  4. Comparable接口将比较代码嵌入自身类中,Comparator在一个独立的类中实现比较。像Integer、String等这些基本类型的JAVA封装类都已经实现了Comparable接口,这些类对象本身就支持自比较,直接调用Collections.sort()就可以对集合中元素的排序,无需自己去实现Comparable接口。
    而有些自定义类的List序列,当这个对象不支持自比较或者自比较函数不能满足你的要求时,你可以写一个比较器来完成两个对象之间大小的比较,也就是指定使用Comparator(临时规则排序,也称作专门规则排序),如果不指定Comparator,那么就用自然规则排序,这里的自然顺序就是实现Comparable接口设定的排序方式。

二.自然排序:java.lang.Comparable

接口描述

总结为一句话:实现Comparable,重写 compareTo方法

public interface Comparable<T>{}

此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo方法被称为它的自然比较方法。实现此接口的对象列表(和数组)可以通过 Collections.sort(和 Arrays.sort)进行自动排序实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparable接口中只定义了一个方法:

public int compareTo(T o);//比较此对象与指定对象的顺序。O为要比较的对象

他即返回值有三种情况:

  • 比较者(调用用该方法的对象)大于被比较者(即CompareTo方法里的参数对象),那么返回1(positive)
  • 比较者等于被比较者,返回0(zero)
  • 比较者小于被比较者,返回-1(negative)
String s1 = "a";
String s2="c";
s1.compareTo(s2);//得到-2说明s1在s2前两位
s2.compareTo(s1);//得到2说明s2在s1后两位

这个接口是对实现他的类自然排序,即常说的字典排序

下面给出一个很简单的程序来说明这个情况:实现Comparable接口的一个类

public class Fruit implements Comparable<Fruit> {
    private String name;
    private  int price;

    public Fruit(String name, int price) {
        this.name=name;
        this.price=price;
    }

    @Override
    public int compareTo(Fruit o) {
        //Integer类也实现了Comparable这个接口
        return ((Integer)price).compareTo(o.price);
    }
}

public class Test {
    public static void main(String[] args) {
        Fruit f1=new Fruit("apple", 5);
        Fruit f2=new Fruit("banana", 7);
        Fruit f3=new Fruit("pear", 3);
        Fruit f4=new Fruit("watermelon", 5);

        System.out.println(f1.compareTo(f2));// -1
        System.out.println(f1.compareTo(f3));// 1
        System.out.println(f1.compareTo(f4));// 0
    }
}


注意一下,前面说实现Comparable接口的类是可以支持和自己比较的,但是其实代码里面Comparable的泛型未必就一定要是对象,将泛型指定为String,或任何类型都可以,只要开发者指定了具体的比较算法就行。

String的compareTo()的源码实现
	private final char value[];//String的底层是字符数组  a.compareTo(b)
    public int compareTo(String anotherString) {
        int len1 = value.length;//获取调用该方法的字符串的长度a
        int len2 = anotherString.value.length;//获取比较字符串的长度b
        int lim = Math.min(len1, len2);//(a <= b) ? a : b;  min底层代码  这句代码是为了获取较短的字符串的长度
        char v1[] = value;  //创建两个字符数组,分别指向这两个字符串的所在
        char v2[] = anotherString.value;
        //循环比较,循环次数,是较短的字符串的长度,如果用较长的字符串的长度,那么会出现nullPointException
        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            //比较相对应索引的元素,如果元素不同则比较返回中间差距的顺序,如果相等,那么就继续循环比较
            if (c1 != c2) {
                return c1 - c2;//字符对应的Unicode码表中的数字,这也就是为什么说String是按照字典书序比较的,如a比b靠前,那么a对应的数字比b小,相减返回负数,差多少顺序,就返回多少
            }
            k++;
        }
        //如果两个字符串的长度不同,其它都相同,那么返回的就是长度的差距了
        return len1 - len2;
    }

我们可以看到String对于compareTo的实现就是依据Unicode码表中字符对应的数字来判断的,返回的是字符串长度差或者是字符间在码表上的差距,依据具体情况,返回也不同.

Integer的compareTo()的源码实现
//Integer的compareTo方法,底层依据的是compare方法,这个方法是Comparator接口的一个方法
    public int compareTo(Integer anotherInteger) {
        //实际上Integer的比较是通过Integer中包括的整数来比较的
        return compare(this.value, anotherInteger.value);
    }
    public static int compare(int x, int y) {//a.compateTo(b)
        //如果a比b小,那么返回-1,相等就是0,否则就是1
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }

根据对String和Integer的compareTo的实现,自己来实现一个类的compareTo方法;

public class ComparableLength implements Comparable<ComparableLength>{
    @Override
    public int compareTo(ComparableLength o) {
        int len1 = Integer.toString(this.hashCode(), 16).length();
        int len2 = Integer.toString(o.hashCode(), 16).length();
        return (len1 - len2 > 0) ? 1 : -1 ;
    }
    
    public String sort(ComparableLength cl){
        if(this.compareTo(cl) == 1){
            return "该对象比后者hashcode长度长";
        }else {
            return "该对象比后者hashcode长度短,也可能相等";
        }
    }


	@Test
    public void testComparable(){
        ComparableLength c1 = new ComparableLength();
        ComparableLength c2 = new ComparableLength();
        ComparableLength c3 = new ComparableLength();
        ComparableLength c4 = c1;
        System.out.println(c1.sort(c2) + c1.hashCode());
        System.out.println(c2.sort(c1));
        System.out.println(c2.sort(c3) + c2.hashCode());
        System.out.println(c1.sort(c3) + c3.hashCode());
        System.out.println(c4.sort(c3) + c4.hashCode());
    }
}

结果
在这里插入图片描述

上面方法简单比较了一下对象的hashcode的长度,接下来我们比较一下hashcode是否相等,和equals()返回同样结果,这也是建议我们做的;

public class ComparableLength implements Comparable<ComparableLength>{
    public String sort(ComparableLength c2){
        if(this.compareTo(c2) == 1)
            return "这两个对象不是同一个对象";
        else 
            return "这两个对象是同一个对象";
    }
    @Override
    public int compareTo(ComparableLength o) {
        String str1 = Integer.toString(this.hashCode());
        String str2 = Integer.toString(o.hashCode());
        int len1 = str1.length();
        int len2 = str2.length();
        char[] ch1 = str1.toCharArray();
        char[] ch2 = str2.toCharArray();
        int lim = len1 - len2 ;
        if(lim == 0){
            int k = 0;
            while (k < len1) {
                char char1 = ch1[k];
                char char2 = ch2[k];
                if(char1 != char2)
                    return 1;
                k++;
            }
        }else if(lim != 0)
            return 1;
        return 0;
    }


	@Test
    public void testComparable(){
        ComparableLength c1 = new ComparableLength();
        ComparableLength c2 = new ComparableLength();
        ComparableLength c3 = new ComparableLength();
        ComparableLength c4 = c1;
        System.out.println(c1.sort(c2) + "--"+c1.equals(c2)+"--"+c1.hashCode());
        System.out.println(c2.sort(c1) + "--"+c2.equals(c1));
        System.out.println(c2.sort(c3) + "--"+c2.equals(c3)+"--"+c2.hashCode());
        System.out.println(c1.sort(c3) + "--"+c1.equals(c3)+"--"+ c3.hashCode());
        System.out.println(c4.sort(c1) + "--"+c4.equals(c1)+"--"+ c4.hashCode());
    }
}

结果
在这里插入图片描述

编写代码Comparable

引入lombok查询,无需手动编写get/set方法

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.20</version>
            <scope>provided</scope>
        </dependency>
  1. 创建Student类实现Comparator
/**
 * 自然排序(内比较器)
 */
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Comparable {
    private int id;
    private String name;

    @Override
    public int compareTo(Object obj) {
        if (obj instanceof Student) {
            Student stu = (Student) obj;
            return id - stu.id;
        }
        return 0;
    }

Student 实现了自然排序接口Comparable,如何利用Comparable接口对Studen数组进行排序的呢?
可以通过使用java.util.Arrays类来给整型数组排序:。
使用 Arrays.sort 方法给整型数组排序。通过API 文档发现, Arrays.sort 方法有很多重载形式,其中就包括 sort(Object[] obj),也就是说 Arryas 也能对对象数组进行排序,排序过程中比较两个对象“大小”时使用的就是 Comparable 接口的 compareTo 方法。

     //调用上面创建实现Comparable接口的Student对象
	public static void main(String[] args) {
        Student stu1 = new Student(1, "Little");
        Student stu2 = new Student(2, "Cyntin");
        Student stu3 = new Student(3, "Tony");
        Student stu4 = new Student(4, "Gemini");

        Student[] studentArr = {stu1,stu4,stu3,stu2};

        System.out.println("Array: "+Arrays.toString(studentArr));
        Arrays.sort(studentArr);
        System.out.println("Sort:"+Arrays.toString(studentArr));
    }
}

执行结果
在这里插入图片描述

studentArr中对象的顺序并不是按id来添加的,Arrays.sort(studentArr )那么如何比较两个对象的大小?

  • 由于Student 实现的Comparable接口sort 方法会将待比较的那个对象强制类型转换成 Comparable ,并调用 compareTo方法,根据其返回值来判断这两个对象的“大小”。所以调用sort 方法后无序数组 studentArr 的顺序根据id进行了升序排序

排序算法和 Student 类绑定了, Student 只有一种排序算法。但现实社会不是这样的,如果我们不想按学号排序,想按姓名来给学生排序怎么办?

  • 我们只能修改 Student 类的 Comparable 接口的 compareTo 方法,改成按姓名排序

如果在同一个系统里有两个操作,一个是按学号排序,另外一个是按姓名排序,这怎么办?

  • 不可能在 Student 类体中写两个 compareTo 方法的实现。这么看来Comparable有局限性了。为了弥补这个不足,JDK 还为我们提供了另外一个排序方式,也就是下面要说的比较器排序

三.比较器排序:java.util.Comparator

接口描述

总结为一句话:实现Comparator接口,重写compare方法

Comparator并不想Comparable那样有许多的实现类,该接口的实现类只有两个,Collator, RuleBasedCollator,但是该接口提供的两个方法,仍然是十分有用的.

接口:

public interface Comparator<T>{}

上面我提到了,之所以提供比较器排序接口,是因为有时需要对同一对象进行多种不同方式的排序,这点自然排序 Comparable 不能实现。另外, Comparator 接口的一个好处是将比较排序算法具体的实体类分离。

  • 通过 API 发现, Arrays.sort 有种重载形式:sort(T[] a, Comparator<? super T> c),这个方法参数的写法用到了泛型。可以把它理解成这样形式: sort(Object[] a, Comparator c),意思就是是按照比较器 c 给出的比较排序算法,对 Object 数组进行排序。

接口方法:

int compare(T o1, T o2) 比较用来排序的两个参数
boolean equals(Object obj) 指示某个其他对象是否“等于”此 Comparator。

//描述compare:
public int compare(Object o1, Object o2)
本来的顺序就是参数的先后顺序o1、o2;
如果保持这个顺序就返回-1,交换顺序就返回1,什么都不做就返回0;
所以 升序的话 如果o1<o2,就返回-1
  • Comparator 接口中定义了两个方法: compare(Object o1, Object o2)equals方法
    • 由于 equals 方法是所有对象都有的方法,因此当我们实现 Comparator 接口时,我们只需重写 compare 方法,而不需重写 equals 方法。
    • Comparator 接口中对重写 equals 方法的描述是:“注意,不重写 Object.equals(Object) 方法总是安全的。然而,在某些情况下,重写此方法可以允许程序确定两个不同的 Comparator 是否强行实施了相同的排序,从而提高性能。”。我们只需知道第一句话就OK了,也就是说,可以不用去想应该怎么实现 equals 方法,因为即使我们不显示实现 equals 方法,而是使用Object类的 equals 方法,代码依然是安全的。
编写代码

那么我们来写个代码,来用一用比较器排序。还是用 Student 类来做,只是没有实现 Comparable 接口。由于比较器的实现类只用显示实现一个方法,因此,我们可以不用专门写一个类来实现它,当我们需要用到比较器时,可以写个匿名内部类来实现 Comparator 。

引入lombok查询,无需手动编写get/set方法

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.20</version>
            <scope>provided</scope>
        </dependency>

创建Student类

/**
 * 比较器排序(外比较器)
 */
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private int id;
    private String name;
}

class ComparatorTest {
    public static void main(String[] args) {
        Student stu1 = new Student(1, "Little");
        Student stu2 = new Student(2, "Cyntin");
        Student stu3 = new Student(3, "Tony");
        Student stu4 = new Student(4, "Gemini");

       	Student[] studentArr = {stu1,stu4,stu3,stu2};
       	System.out.println("Array: " + Arrays.toString(studentArr));

        Arrays.sort(studentArr, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof Student && o2 instanceof Student) {
                    Student s1 = (Student) o1;
                    Student s2 = (Student) o2;
                    //return s2.getId() - s1.getId(); // 按Id排
                    return s1.getName().compareTo(s2.getName()); // 按姓名排
                }
                return 0;
            }
        });

        System.out.println("Sorted: " + Arrays.toString(studentArr));
    }
}

当需要Student按学号排序时,只需修改我实现Comparator的内部类中的代码,而不用修改 Student 类。

注意: 当然,你也可以用 Student 类实现 Comparator 接口,这样Student就是一个比较器了(Comparator)。当需要使用这种排序的时候,将 Student 看作 Comparator 来使用就可以了,可以将 Student 作为参数传入 sort 方法,因为 Student is a Comparator 。但这样的代码不是个优秀的代码,因为我们之所以使用比较器(Comparator),其中有个重要的原因就是,这样可以将比较算法和具体类分离,降低类之间的耦合。

四.TreeSet对Comparable(自然排序)和Comparator(比较器排序)支持

TreeSet对这两种比较方式都提供了支持,分别对应着TreeSet的两个构造方法:

  1. TreeSet():根据TreeSet中元素实现的 Comparable 接口的 compareTo 方法比较
  2. TreeSet(Comparator comparator):根据给定的 comparator 比较器,对 TreeSet 中的元素比较排序
测试TreeSet对Comparable的支持
/**
 * 自然排序(内比较器)
 */
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Comparable {
    private int id;
    private String name;

    @Override
    public int compareTo(Object obj) {
        if (obj instanceof Student) {
            Student stu = (Student) obj;
            return id - stu.id;
        }
        return 0;
    }


    /**
     * 使用自然排序
     * Student必须实现Comparable接口,否则会抛出ClassCastException
     */
    public static void main(String[] args) {
        Student stu1 = new Student(1, "Little");
        Student stu2 = new Student(2, "Cyntin");
        Student stu3 = new Student(3, "Tony");
        Student stu4 = new Student(4, "Gemini");

        SortedSet set = new TreeSet();
        set.add(stu1);
        set.add(stu3); // 若Student没有实现Comparable接口,抛出ClassCastException
        set.add(stu4);
        set.add(stu2);
        set.add(stu4);

        System.out.println(set);
    }
}

测试结果
在这里插入图片描述

测试TreeSet对Comparator的支持

/**
 * 比较器排序(外比较器)
 */
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private int id;
    private String name;

    /**
     * 使用比较器排序
     * Student可以只是个简单的Java类,不用实现Comparable接口
     */
    public static void main(String[] args) {
        Student stu1 = new Student(1, "Little");
        Student stu2 = new Student(2, "Cyntin");
        Student stu3 = new Student(3, "Tony");
        Student stu4 = new Student(4, "Gemini");

        SortedSet set = new TreeSet(new Comparator() {

            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof Student
                        && o2 instanceof Student) {
                    Student s1 = (Student) o1;
                    Student s2 = (Student) o2;
                    return s1.getName().compareTo(s2.getName());
                }
                return 0;
            }

        });

        set.add(stu1);
        set.add(stu3);
        set.add(stu4);
        set.add(stu2);
        set.add(stu4);

        System.out.println(set);
    }
}

测试结果:
在这里插入图片描述

五.总结

  1. java.util.Arrays提供了一系列用于对数组操作的静态方法,查找排序等等。
  2. java.util.Collections也提供了一系列这样的方法,只是它是用于处理集合的,它不是只能处理Collection接口以及子接口的实现类,同样也可以处理Map接口的实现类。

https://www.cnblogs.com/lin-jing/p/8278271.html
https://www.cnblogs.com/lin-jing/p/8279205.html
https://blog.csdn.net/sinat_37976731/article/details/79902968
https://www.cnblogs.com/andywithu/p/7239613.html
https://blog.csdn.net/boy_chen93/article/details/84571984
https://blog.csdn.net/diweikang/article/details/80788194
https://blog.csdn.net/awhip9/article/details/61918072

发布了62 篇原创文章 · 获赞 109 · 访问量 5311

猜你喜欢

转载自blog.csdn.net/qq877728715/article/details/102679040