1. 程式人生 > >Java中類的比較與排序方法(應用Comparable介面與Comparator介面)

Java中類的比較與排序方法(應用Comparable介面與Comparator介面)

引言

在平時寫Java的程式的時候,如果要進行一些基本型別的變數的比較,可以很方便得呼叫’Math.max()’、Math.min()等方法,如果要對陣列或者列表進行排序,也可以用Arrays.sort()Collections.sort()等已經封裝好的方法來進行。但是,如果是一個自定義的類的物件呢?比如自定義的兩個圖形、兩個日期等,這時,應該怎麼對這些物件進行大小比較乃至排序呢?

基本型別及其包裝類的排序

在介紹自定義的類的比較與排序之前,還是先簡單回顧一下Java中的基本型別的資料與其包裝類的元素所組成的陣列的排序方式:

public static void
primitiveDataTypeSort() { // 基本資料型別可以直接排序 int[] ints = {3, 1, 5, 2, 4}; java.util.Arrays.sort(ints); for(int number: ints) System.out.print(number + " "); System.out.println(); // 包裝器型別(即物件形式)也可以直接進行排序 Character[] characters = { new Character
('a'), new Character('c'), new Character('d'), new Character('b'),}; java.util.Arrays.sort(characters); for (Character character: characters) { System.out.print(character + " "); } }

輸出:

1 2 3 4 5
a  b  c  d

對於List中的類:

public static void collectionsSortTest() {
        List<Double> doubleList = new ArrayList<>();
        doubleList.add(10.5);
        doubleList.add(-0.5);
        doubleList.add(1.5);
        for (double num: doubleList) System.out.print(num + " ");
        System.out.println();
        Collections.sort(doubleList);
        for (double num: doubleList) System.out.print(num + " ");
    }

輸出:

10.5 -0.5 1.5
-0.5 1.5 10.5

可以看到,對於基本型別與其包裝類的物件而言,應用Java自帶的Arrays.sort()Collections.sort()可以很方便地對其進行排序操作。

使用Comparable介面

Comparable介面定義了compareTo方法,用於物件之間的比較

為了實現這樣自定義物件的排序,我們可以將這樣的類定義為“可比較”的,為了實現這樣的要求,應該使每個物件可以呼叫一個共同的比較方法。在Java中已經對於這樣的“可比較”做了定義,即規定了Comparable介面來對“可比較”進行了抽象,因此對於我們希望其例項之間可以相互比較的自定義的類,需要實現Comparable介面,使每個物件都有共同方法comparable
Comparable介面的定義如下所示:

package java.lang;
import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

compareTo方法判斷這個物件相對於給定物件o的順序,當這個物件小於、等於或大於給定物件o時,分別返回負整數、0或正整數。

Comparable介面是一個反省介面。在實現該介面時,泛型型別E被替換成一種具體的型別。Java類庫中的許多類實現了Comparable介面以定義物件的自然順序。Byte、Short、Integer、Long、Float、Doule等基本型別的包裝類,Character、BigInteger、BigDecimal、Calendar、String以及Date這樣常用的類,都實現了Comparable介面。

如在Java API中,Integer類中有如下定義:

public final class Integer extends Number implements Comparable<Integer> {
    //... class body omitted..

    @Override
    public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
    }
}

因此,整型數字時可以比較的,類似的,其它型別的數字也是可以比較的,字串是可以比較的,日期也是如此。若需要直接比較其大小,可以使用compareTo方法來進比較。

對於compareTo方法直接進行呼叫的效果如下:

public static void compareToTest() {
    System.out.println(new Integer(3).compareTo(new Integer(5)));
    System.out.println("ABC".compareTo("ABE"));
    java.util.Date date1 = new java.util.Date(2013, 1, 1);
    java.util.Date date2 = new java.util.Date(2012, 1, 1);
    System.out.println(date1.compareTo(date2));
}

輸出:

-1
-2
1

第一行顯示一個負數,因為3小於5,第二行i按時一個負數,因為ABC小於ABE,最後顯示一個正數,因為date1大於date2。

由於所有Comparable物件都有compareTo()方法,如果物件是Comparable介面型別的例項的話,Java API中的java.util.Arrays.sort(Object[])方法就可以使用compareTo()方法來對陣列中的物件進行比較和排序。

下面,我們來自定義一個類來實驗這個介面的用法。首先,我們定義一個圓圈的類Circle

public class Circle {
    private double radius;

    public Circle() { this.radius = 0; }

    public Circle(double radius) { this.radius = radius;}

    public double getArea() { return Math.PI * radius * radius; }

    public void getName() {System.out.print("  Circle:" + this.radius);}
}

這時,如果我們定義一個Circle型別的陣列,然後直接對其進行排序,就會得到一個“未實現Comparable介面”的錯誤,如下:

public static void main(String[] args) {
    Circle[] circles = new Circle[] {new Circle(4), new Circle(3), new Circle(5)};
    for (Circle circle: circles)
        System.out.println(circle.getArea());
    Arrays.sort(circles);
}

輸出:

50.26548245743669
28.274333882308138
78.53981633974483
Exception in thread "main" java.lang.ClassCastException: learning_java.sortTry.Circle cannot be cast to java.lang.Comparable

由輸出的前三行顯示可以看到我們已經成功地得到了三個圓圈的例項,但是由於Circle類並沒有實現Comparable介面,因此不能直接使用Arrays.sort()方法來對這個陣列進行排序。

這時,我們重新定義一個實現了Comparable介面的圓圈的類CircleComparable

public class CircleComparable
        extends Circle
        implements Comparable<CircleComparable> {

    public CircleComparable(double radius) {
        super(radius);
    }

    public int compareTo(CircleComparable circleToCom) {
        if (this.getArea() > circleToCom.getArea())
            return 1;
        else if (this.getArea() < circleToCom.getArea())
            return -1;
        else
            return 0;
    }
}

在上面的程式碼中,定義了一個擴充套件自Circle的類CircleComparable,並在這個擴充套件類中實現了Comparable介面,用compareTo(CircleComparable circleToCom)方法來比較兩個圓的面積。因此,CircleComparable類的例項既是自己的一個例項,也是CircleComparable的例項,當然也是Object的一個例項。接下來再來對其進行一次測試:

public static void main(String[] args) {
    CircleComparable c = new CircleComparable(1);
    System.out.println("c instanceof CircleComparable:\t" + (c instanceof CircleComparable));
    System.out.println("c instanceof Circle:\t" + (c instanceof Circle));
    System.out.println("c instanceof Comparable:\t" + (c instanceof Comparable));
    System.out.println("c instanceof Object:\t" + (c instanceof Object));
    Circle[] circles = new CircleComparable[] {new CircleComparable(4), new CircleComparable(3), new CircleComparable(5)};
    for (Circle circle: circles)
        System.out.print(circle.getArea() + " ");
    System.out.println();
    Arrays.sort(circles);
    for (Circle circle: circles)
        System.out.print(circle.getArea() + " ");
}

輸出:

c instanceof CircleComparable:	true
c instanceof Circle:	true
c instanceof Comparable:	true
c instanceof Object:	true
50.26548245743669 28.274333882308138 78.53981633974483
28.274333882308138 50.26548245743669 78.53981633974483

可以看到,排序後我們得到了正確的升序輸出。

介面提供了通用程式設計的一種形式,在這個例子中,如果不同介面,很難使用通用的sort()方法來排序物件,因為必須使用多重繼承才能同時繼承Comparable和其基類。

在這一部分的最後再提一點,Object類包含equals方法,它的目的就是為了讓Object類的子類來覆蓋它,以比較物件的內容是否相同。假設Object類包含一個類似於Comparable介面中所定義的comnpareTo方法,那麼sort方法可以用來比較一組任意的物件。Object類中是否應該包含一個compareTo方法尚有爭論,由於在Object類中沒有定義compareTo方法,所以Java中定義了Comparable介面,以便能夠對兩個Comparable介面的例項物件進行比較。在這裡,雖然即使不遵守,編譯器也不會進行報錯,但是強烈建議compareTo應該與equals保持一致,即對於兩個物件o1o2o1.equals(o2)trueo1.compareTo(o2)==0成立的條件應該相同。

使用Comparator介面

Comparator可以用於比較沒有實現Comparable的類的物件

在前一節中我們已經實現了使用Comparable介面來比較元素,Java API中的許多常用的類,比如Number的子類Integer、BigInteger、BigDecima等與String、Date等類,都實現了Comparable介面,因此,這些類可以直接用於比較。

但是,如果我們所要處理的元素的類沒有實現Comparable介面呢?此時這些元素可以進行比較麼?在上面,我們舉了一個例子,一個沒有實現Comparable介面的類Circle,並建立了一個Circle型別的陣列,實驗表明,並不能直接將其進行排序,那麼是不是對於這些類就不能實現比較與排序了呢?

答案是我們可以通過定義一個比較器(comparator)來實現不同類的元素的比較。要做到這一點,需要建立一個實現java.util.Comparator<T>介面的類並重寫它的cmopare方法。

在這裡,我們先來看一下這個Comparator介面的定義:

public interface Comparator<T> {
    // ...
    int compare(T o1, T o2);
    // ..
}

之後,我們可以通過實現這個介面來寫一個Circle類的比較器CircleComparator

import java.util.Comparator;

public class CircleComparator
        implements Comparator<Circle> {
    public int compare(Circle o1, Circle o2) {
        if (o1.getArea() > o2.getArea())
                return 1;
        else if (o1.getArea() < o2.getArea())
            return -1;
        else
            return 0;
    }
}

在這個類中,我們通過覆蓋compare方法來實現了Comparator介面,完成了一個Circle的比較器。在compare()方法中,對輸入的兩個Circle的物件的面積進行判斷,若第一個圓的面積大於第二個圓的面積,則返回1,反之則返回-1,而若兩個圓的面積相同,則返回0。

若簡單地呼叫這個比較器:

Circle circleOne = new Circle(3);
Circle circleTwo = new Circle(5);
CircleComparator comparator = new CircleComparator();
System.out.println(comparator.compare(circleOne, circleTwo)); //的一個圓比第二個圓的面積小,則會返回-1

則輸出:

-1

而若我們想要像前面的例子一樣,實現陣列的排序,我們可以先看一下`Arrays’類的部分原始碼:

public static <T> void sort(T[] a, Comparator<? super T> c) {
    if (c == null) {
        sort(a);
    } else {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, c);
        else
            TimSort.sort(a, 0, a.length, c, null, 0, 0);
    }
}

可以看到上面這段程式碼中是’Arrays.sort()'的一種實現,在這個方法中,輸入的第二個引數就是我們上面所構造的比較器。而這種陣列的排序方法,其實就是按照我們所寫的對於物件的比較方法,來對陣列進行排序。

其應用可以看如下例子:

public static void circleSortTest2() {
    Circle[] circles = {new Circle(3), new Circle(1), new Circle(2)};
    java.util.Arrays.sort(circles, new CircleComparator());
    for (Circle circle: circles) {
        System.out.print(circle.getArea() + "  ");
    }
}

輸出:

3.141592653589793  12.566370614359172  28.274333882308138

從輸出可以看到,3個圓類的物件確實按照面積大小進行了升序排列。

結合匿名內部類Comparator介面

如果在程式中我們只是在某個地方需要使用一次Comparator介面來對某個沒有實現Comparable介面的類的物件的陣列或列表來進行比較或者排序,這時,可能會覺得需要專門寫一個類似乎有些麻煩。或者說,我們在呼叫一個不能排序的物件的時候,想要將它可以排序這個特性封裝到一個類中(比如在leetcode上做題的時候,只能提交一個類),這時,我們便可以結合內部類匿名內部類來編寫程式碼。

這裡給出一個例子,還是對於上面我們的圓來進行排序,但是,在這裡我們使用匿名內部類來完成這個操作。定義在另一個類的內部的類便被稱為內部類,而沒有顯式地寫出其類名的內部類便被稱為匿名內部類,其詳細解釋可以看匿名內部類裡的解釋。

程式碼:

import java.util.*;

public class TestSort {
    public static void main(String[] args) {
        CircleSortWithInner();
    }

    public static void CircleSortWithInner() {
        Circle circle1 = new Circle(3);
        Circle circle2 = new Circle(1);
        Circle circle3 = new Circle(4);
        Circle circle4 = new Circle(2);
        Circle[] circles = {circle1, circle2, circle3, circle4};
        for (Circle circle: circles) circle.getName();
        Arrays.sort(circles, new Comparator<Circle>() {
            @Override
            public int compare(Circle o1, Circle o2) {
                if(o1.getArea() > o2.getArea()) return 1;
                else if(o1.getArea() < o2.getArea()) return -1;
                else return 0;
            }
        });
        System.out.println();
        for (Circle circle: circles) circle.getName();
    }
}

輸出:

Circle:3.0  Circle:1.0  Circle:4.0  Circle:2.0
Circle:1.0  Circle:2.0  Circle:3.0  Circle:4.0

可以看到,這個圓的陣列已經被正確地排序了。

在這個例子中,向Arrays.sort方法中傳入的第二個引數,應為Comparator介面的一個物件,這裡就是用匿名內部類來實現定義這樣一個實現Comparator介面的類並生成一個其物件並將其傳入方法。

如果只需使用一次的話,這種定義比較器的方法還是比較方便的,但是也會讓程式碼的可讀性下降,因此使用的時候需要權衡一下。