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:	" + (c instanceof CircleComparable));
    System.out.println("c instanceof Circle:	" + (c instanceof Circle));
    System.out.println("c instanceof Comparable:	" + (c instanceof Comparable));
    System.out.println("c instanceof Object:	" + (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接口的类并生成一个其对象并将其传入方法。

如果只需使用一次的话,这种定义比较器的方法还是比较方便的,但是也会让代码的可读性下降,因此使用的时候需要权衡一下。