Java进阶知识6:利用Comparable或Comparator接口实现对象排序

          前言:排序算法是我们初学编程时常用的算法,每个人都尝试过排序,但只是局限于基本数据类型的简单排序。比如:将下列数字进行排序1,3,5,8,3,6,于是我们得出结果1,3,3,5,6,8。将下列字母(字符)进行排序  a,i,e,f,w,s  于是我们得出结果  a,e,f,i,s,w。但是我们工作后遇到的情况就不是如此简单了。比如给公司里的商品进行排序,我们很轻易的想到按照商品的名称排序不就完了,而且简单明了,但现实并如我们相信那般简单。同一商品名称可以有不同的进货时间,可能还会有进货单价的不同,这显然只根据商品名称排序是不合理的。所以在实际应用中,我们往往有需要比较两个自定义对象大小的地方。而这些自定义对象的比较,就不像简单的整型数据那么简单,它们往往包含有许多的属性,我们一般都是根据这些属性对自定义对象进行比较的。所以Java中要比较对象的大小或者要对对象的集合进行排序,需要通过比较这些对象的某些属性的大小来确定它们之间的大小关系。

         这就涉及到我们下面要讲的如何给自定义类的对象进行排序,利用排序接口Comparable或者比较器接口Comparator。首先类要实现接口,并且使用泛型规定要进行比较的对象所属的类,然后类实现了接口后,还需要实现接口定义的比较方法(compareTo方法或者compare方法),在这些方法中传入需要比较大小的另一个对象,通过选定的成员变量与之比较,如果大于则返回1,小于返回-1,相等返回0。

一、java.lang.Comparable接口简介

1.1、Comparable接口应用背景

public final class Integer extends Number implements Comparable<Integer> {...}

今天在开发中无意看到Integer包装类内部实现了Comparable接口,因此探查一下API解释:

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

为什么需要实现这个接口呢?

首先看一下数据的例子:

    String[] strArr = {"A","B","C","E","D"};
    Arrays.sort(strArr);
    for (String string : strArr) {
        System.out.print(string+";");
    }

输出结果:

A;B;C;D;E;

从中我们可以看出sort方法对数据中的String字符串按照一定规则进行了排序,那为什么会排序呢?

查看String类源码我们可以看到:

  public final class String
    implements Serializable, Comparable, CharSequence{
        ...
    }

它也实现了Comparable接口。里面实现了compareTo方法,所以按照某种规则能够进行排序。

如果数组中的对象不是String而是自定义的类型呢?  

  public class ComparableDemo{
        public static void main(String[] args) {
            Object[] objArray = {new Person(20,"jack"),new Person(17,"tom"),
                                new Person(27,"aj")};
            for (Object object : objArray) {
            System.out.print(object.toString());
            }
            Arrays.sort(objArray);
            for (Object object : objArray) {
                System.out.print(object.toString());
            }
            
        }
    }

    public class Person {
        private Integer age;
        private String name;
        public Person(int age,String name) {
            this.age = age;
            this.name = name;
        }
        @Override
        public String toString() {
            return "Person [age=" + age + ", name=" + name + "]";
        }
        
    }

结果为:

    Person [age=20, name=jack]Person [age=17, name=tom]Person [age=27, name=aj]
    Exception in thread "main" java.lang.ClassCastException: interfacedemo.Person cannot be cast to java.lang.Comparable
        at java.util.ComparableTimSort.countRunAndMakeAscending(Unknown Source)
        at java.util.ComparableTimSort.sort(Unknown Source)
        at java.util.ComparableTimSort.sort(Unknown Source)
        at java.util.Arrays.sort(Unknown Source)
        at interfacedemo.ComparableDemo.main(ComparableDemo.java:18)

可以看到不进行排序打印是正常的,但是排序时报错了。因为系统无法知道使用什么规则进行排序。

我们存入字符串排序成功是因为String类已经实现Comparable接口,因此想要实现自定义对象比较同样需要实现该Comparable接口,其中的比较方法规则由我们自己设定。

1.2、Comparable排序接口总结

该接口定义如下:

package java.lang;
import java.util.*;
public interface Comparable<T> 
{
    public int compareTo(T o);
}

     Comparable是一个泛型接口。Java类库中:Byte,Short,Integer,Long,Float,Double,Character,BigTnteger,BigDecimal,Calendar,String及Data类都实现了Comparable接口,这些基本类型对象可以直接调用进行排序。

    Comparable是排序接口若一个类实现了Comparable接口,就意味着该类支持排序。实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序。此外,实现此接口的对象可以用作有序映射表中的键或有序集合中的集合,无需指定比较器。

    Comparable接口的意义:在排序时,Java本身提供的一些类,它们已经实现comparable接口,可以直接进行对象比较。但是在List容器里添加自定义的对象,就没法直接调用Collections.sort()方法了,编译期不知道按照什么来进行排序,会报错,这个时候就必须用到Comparable接口。

   实现了Comparable接口的类,需要实现compareTo()方法。jdk中的默认排序都是通过判断此接口实现,通过该接口的compareTo方法返回值比较大小排序。如Collections.Sort,及TreeMap中的排序都是默认通过compareTo的返回值进行排序。Comparable常被称为内比较器

int  CompareTo(T object)方法

Java提供Comparable接口为我们解决比较两个对象的问题。

接口中的compareTo(T object)方法判断这个象相对于比较对象的顺序,小于返回负整数,等于返回0,大于返回正整数。

1.3、Comparable排序接口的用法实例

package coreJavaOne;
 
import java.util.Arrays;
 
public class User implements Comparable<User> {
    private String id;
    private int age;
 
    public User(String id, int age) {
        this.id = id;
        this.age = age;
    }
 
    public int getAge() {
        return this.age;
    }
 
    public String getId() {
        return this.id;
    }
 
    public void setId(String id) {
        this.id = id;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    public String toString() {
        return "[ id = " + this.id + ", age = " + this.age + "]";
    }
 
    public int compareTo(User other) {
        return this.age - other.getAge();
    }
 
    public static void main(String args[]) {
        User[] users = {new User("wangzhengyi", 26), new User("bululu", 27)};
 
        Arrays.sort(users);
 
        for (int i = 0; i < users.length; i++) {
            System.out.println(users[i]);
        }
    }
}

输出结果:

二、 java.util.Comparator简介

2.1、Comparator比较器接口总结

与上面的Comparable接口不同的是:

Comparator位于包java.util下,而Comparable位于包java.lang下。

Comparable接口将比较代码嵌入需要进行比较的类的自身代码中,而Comparator接口在一个独立的类外实现比较。

如果前期类的设计没有考虑到类的Compare问题而没有实现Comparable接口,后期可以通过Comparator接口来实现比较算法进行排序,并且为了使用不同的排序标准做准备,比如:升序、降序。

Comparable接口强制进行自然排序,而Comparator接口不强制进行自然排序,可以指定排序顺序。

  Comparator是比较器接口我们如果需要控制某个自定义类的对象次序,而该类本身不支持排序(即没有实现Comparable接口),那么我们就可以建立一个“该类的比较器”来进行排序,这个“比较器”只需要实现Comparator接口即可。也就是说,我们可以通过实现Comparator来新建一个比较器,然后通过这个比较器对类进行排序。Comparator相当于外部比较器。

该接口定义如下:

package java.util;
public interface Comparator<T>
 {
    int compare(T o1, T o2);
    boolean equals(Object obj);
 }

注意:1、若一个类要实现Comparator接口:它一定要实现compare(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数。

   2、int compare(T o1, T o2) 是“比较o1和o2的大小”。返回“负数”,意味着“o1比o2小”;返回“零”,意味着“o1等于o2”;返回“正数”,意味着“o1大于o2“

使用场景:
       ① 类虽然已实现Comparable接口,但是我对他的比较规则不满意,我想根据我自己的业务情况重新定义比较规则。这时候可以使用匿名内部类的方式来达到我们的目的。
       ②某个类我们手里只有字节码文件(拿不到源代码),不管这个类没有实现Comparable接口,我们可以通过Comparator接口达到比较的目的。       

2.2、java.util.Comparator 比较器接口的用法实例

示例①:

package coreJavaOne;
 
import java.util.Arrays;
import java.util.Comparator;
 
class User implements Comparable<User> {
    private String id;
    private int age;
 
    public User(String id, int age) {
        this.id = id;
        this.age = age;
    }
 
    public int getAge() {
        return this.age;
    }
 
    public String getId() {
        return this.id;
    }
 
    public void setId(String id) {
        this.id = id;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    public String toString() {
        return "[ id = " + this.id + ", age = " + this.age + "]";
    }
 
    public int compareTo(User other) {
        return this.age - other.getAge();
    }
}
 
public class SampleComparator implements Comparator<User> {
    /**
     * 重写接口方法,按照年龄从大到小排序(注意:Comparable示例是按照年龄从小到大排序)
     */
    public int compare(User o1, User o2) {
        return o2.getAge() - o1.getAge();
    }
    
    public static void main(String args[]) {
        User[] users = {new User("wangzhengyi", 26), new User("bululu", 27)};
        
        Arrays.sort(users, new SampleComparator());
        
        for (int i = 0; i < users.length; i ++) {
            System.out.println(users[i]);
        }
    }
}

输出结果:

示例②:

public class UserInfo {

private int id;

private String name;

private int age;

public UserInfo(int id, String name, int age) {

this.id = id;

this.name = name;

this.age = age;

}

@Override

public String toString() {

return "UserInfo[id=" + id + " name=" + name + " age=" + age + "]";

}

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

}

public class InterTest {

public static void main(String[] args) {

List<UserInfo> userInfoList = new ArrayList<>();

userInfoList.add(new UserInfo(0, "大强", 12));

userInfoList.add(new UserInfo(1, "大黄", 18));

userInfoList.add(new UserInfo(2, "大黑", 16));

userInfoList.add(new UserInfo(0, "大白", 52));

userInfoList.add(new UserInfo(0, "红红", 8));

userInfoList.add(new UserInfo(0, "翠花", 16));

userInfoList.add(new UserInfo(0, "芹菜", 36));

System.out.println("排序前");

for (UserInfo userInfo : userInfoList) {

System.out.println(userInfo);

}

Collections.sort(userInfoList, new Comparator<UserInfo>() {

@Override

public int compare(UserInfo o1, UserInfo o2) {

return o1.getAge() > o2.getAge() ? 1 : (o1.getAge() == o2.getAge() ? 0 : -1);

}

});

System.out.println("排序后");

for (UserInfo userInfo : userInfoList) {

System.out.println(userInfo);

}

}

}

输出结果:

排序前

UserInfo[id=0 name=大强 age=12]

UserInfo[id=1 name=大黄 age=18]

UserInfo[id=2 name=大黑 age=16]

UserInfo[id=0 name=大白 age=52]

UserInfo[id=0 name=红红 age=8]

UserInfo[id=0 name=翠花 age=16]

UserInfo[id=0 name=芹菜 age=36]

排序后

UserInfo[id=0 name=大红 age=8]

UserInfo[id=0 name=大强 age=12]

UserInfo[id=2 name=大黑 age=16]

UserInfo[id=0 name=翠花 age=16]

UserInfo[id=1 name=大黄 age=18]

UserInfo[id=0 name=芹菜 age=36]

UserInfo[id=0 name=大白 age=52]

三、Comparable和Comparator区别比较

     Java 之所以出现了java.lang.Compareable和java.util.Comparator两个接口,前者是为了当你写一个类时,你要是要使它能够排序,可以选择实现;当你没实现,而后来又想实现排序功能时,两种解决办法,一是重写此类,让它实现Compareable接口,但一般不这么用,为了满足设计模式开闭原则,所以选择后者,写一个此类的比较类,实现Comparator接口。

  Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器接口,我们若需要控制某个类的次序,且该类没有实现Comparable接口,可以建立一个“该类的比较器”来进行排序。Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。

  两种方法各有优劣, 用Comparable 简单, 只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。 用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator 里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。

1. 相同点

    二者都是接口,而且都可以实现比较,进而实现排序。

    简单的说:

             实现Comparable接口可以直接使用sort方法Collections.sort(List list)排序。

             实现Comparator接口可以用Collections.sort(List list, Comparator c)排序。

2. 不同点

   Comparable接口的耦合性较强一些,必须要在类的源代码里面进行修改。

   而Comparator接口则灵活性强一些,它可以在我们没有源代码的情况下,可以实现我们自定义的排序规则。

       一个类实现了Camparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。

    Comparator可以看成是一种外部算法的实现,将算法和数据分离,Comparator也可以在下面两种环境下使用:

  1. 类的设计师没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序从而不必改变对象本身。
  2. 可以使用多种排序标准,比如升序、降序等。

四、Collections如何调用重写的compareTo()方法的

集合框架中,Collections工具类支持两种排序方法:如果待排序的列表中是数字或者字符,可以直接使用Collections.sort(list);当需要排序的集合或数组是自定义类的对象时,需要自己定义排序规则,实现一个Comparator比较器。

Collections.sort(List<T> list);
Collections.sort(List<T> list, Comparator<? super T> c)    

Collections.sort(list)方法,方法传递一个List集合,这里要求List泛型里面装的元素必须实现Compareable接口。也就是列表中的所有元素都必须是可相互比较的(也就是说,对于列表中的任何 e1 和 e2 元素,e1.compareTo(e2) 不得抛出 ClassCastException)。

3.1 Collections.sort源码

 public static <T extends Comparable<? super T>> void sort(List<T> list) {
        Object[] a = list.toArray();
        Arrays.sort(a);
        ListIterator<T> i = list.listIterator();
        for (int j=0; j<a.length; j++) {
            i.next();
            i.set((T)a[j]);
        }
    }

由源码可以看出来,sort内部调用了Arrays.sort的方法,继续向下看
3.2 Arrays.sort源码

Java Arrays中提供了对所有类型的排序。其中主要分为Primitive(8种基本类型)和Object两大类。

  基本类型:采用调优的快速排序;

    对象类型:采用改进的归并排序。

public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a);
    }

源码里首先判断是否采用传统的排序方法,LegacyMergeSort.userRequested属性默认为false,也就是说默认选中 ComparableTimSort.sort(a)方法(传统归并排序在Java 1.5及之前是默认排序方法,Jav1.5之后默认执行ComparableTimSort.sort()方法。除非程序中强制要求使用传统归并排序,语句如下:System.setProperty("java.util.Arrays.useLegacyMergeSort", "true"))
继续看 ComparableTimSort.sort(a)源码
3.3 ComparableTimSort.sort(a)源码

public static void sort(Object[] a) {
          sort(a, 0, a.length);
    }

  public  static void sort(Object[] a, int lo, int hi) {
        rangeCheck(a.length, lo, hi);
        int nRemaining  = hi - lo;
        if (nRemaining < 2)
            return;  // Arrays of size 0 and 1 are always sorted

        // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi);
            binarySort(a, lo, hi, lo + initRunLen);
            return;
        }

        /**
         * March over the array once, left to right, finding natural runs,
         * extending short natural runs to minRun elements, and merging runs
         * to maintain stack invariant.
         */
        ComparableTimSort ts = new ComparableTimSort(a);
        int minRun = minRunLength(nRemaining);
        do {
            // Identify next run
            int runLen = countRunAndMakeAscending(a, lo, hi);
            
            // If run is short, extend to min(minRun, nRemaining)
            if (runLen < minRun) {
                int force = nRemaining <= minRun ? nRemaining : minRun;
                binarySort(a, lo, lo + force, lo + runLen);
                runLen = force;
            }

            // Push run onto pending-run stack, and maybe merge
            ts.pushRun(lo, runLen);
            ts.mergeCollapse();
            
            // Advance to find next run
            lo += runLen;
            nRemaining -= runLen;
        } while (nRemaining != 0);

        // Merge all remaining runs to complete sort
        assert lo == hi;
        ts.mergeForceCollapse();
        assert ts.stackSize == 1;
    }

nRemaining表示没有排序的对象个数,方法执行前,如果这个数小于2,就不需要排序了。
如果2<= nRemaining <=32,即MIN_MERGE的初始值,表示需要排序的数组是小数组,可以使用mini-TimSort方法进行排序,否则需要使用归并排序。
mini-TimSort排序方法:先找出数组中从下标为0开始的第一个升序序列,或者找出降序序列后转换为升序重新放入数组,将这段升序数组作为初始数组,将之后的每一个元素通过二分法排序插入到初始数组中。注意,这里就调用到了我们重写的compareTo()方法了。

  private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {
        assert lo < hi;
        int runHi = lo + 1;
        if (runHi == hi)
            return 1;

        // Find end of run, and reverse range if descending
        if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)
                runHi++;
            reverseRange(a, lo, runHi);
        } else {                              // Ascending
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)
                runHi++;
        }

        return runHi - lo;
    }

参考链接:

Java中Comparable接口和Comparator接口的比较

Comparator和Comparable的区别使用

猜你喜欢

转载自blog.csdn.net/CSDN2497242041/article/details/106822167