Java 中那些绕不开的内置接口 -- Comparable 和 Comparator

4,303 阅读8分钟

本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

上一篇 用 Java 编程那些绕不开的接口这个短系列的第二篇文章,我们讲述了 Iterable 和 Iterator 两个名字有点像的 Java 内置接口。

前文回顾:Java 中那些绕不开的内置接口 -- Iterator 和 Iterable

恰巧今天要介绍的两个Java 内置接口在名字上乍一看也有点让人分不清楚,他们是 Comparable 和 Comparator 接口。如果你英文还可以应该能猜出来两者的区别,这篇我会用一些示例给大家解释清楚这两个接口的作用。

本文内容大纲如下:

前言

Java 的 Comparable 接口( java.lang.Comparable)表示一个可比较对象--即可以与其他对象进行比较的对象。例如,数字是可比较的,字符串也是可以使用字母序比较的等等。Java 中的类型的包装类和一些内置类都已经实现了 Java Comparable 接口,支持进行对象间的比较。我们也可以自己实现 Comparable 接口,使自己定义的类具有可比性。

在 Java 中,可比较的对象在放在一个集合里(List 或者 Set),那么就支持在集合里对它们进行排序。比如一个存放 String 类型元素的 List

List<String> list = new ArrayList<>();

list.add("c");
list.add("b");
list.add("a");

Collections.sort(list);

这个 List 中的元素类型都是 String类型,Java 的 String 类实现了 Comparable 接口,所以使用 Collections 接口的 sort() 方法对元素进行排序的时候,就会按照 String 类型对 Comparable接口的实现逻辑规则,逐个比较完成排序。

而 Java 的 Comparator 接口(java.util.Comparator)表示可以比较两个对象的组件--比较器,因此可以使用 Java 中的排序功能对它们进行排序。比如使用 Collections.sort 方法排序 List 时,可以将比较器传递给排序方法。在排序过程中会使用 Comparator 比较 List 中的对象。

和上一篇讨论的 Iterable 和 Iterator 接口类似,Comparable 和 Comparator 又是两个名字近似的接口,在这篇文章里,我们把这两个接口也给大家梳理明白,避免混淆。

Comparable 接口

当一个类实现了 Comparable 接口时,这意味着该类的实例可以相互比较。当对象是可比较的时候,就可以使用 Java 中的内置排序功能对它们进行排序。

List<String> list = new ArrayList<>();

list.add("c");
list.add("b");
list.add("a");

Collections.sort(list);

比如上面例程里List 的元素是 String类型的,而 Java 的 String 类实现了 Comparable 接口,所以可以使用 Collections.sort() 方法按自然顺序对列表内的元素进行排序。

需要注意的是,Comparable 接口用于比较同一个类的对象。举个形象的例子说就,是将苹果与苹果进行比较,橙子与橙子进行比较。而不是将苹果与橙子进行比较,或将字符串与数字、日期与车牌等不同类的实例进行比较

Comparable 接口的定义

Comparable 接口位于 java.lang 包内,其定义如下:

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

Comparable 接口只定义了一个 CompareTo 方法,下面将解释 compareTo() 方法的工作原理。

CompareTo 方法

因为 Comparable 接口支持泛型,compareTo() 方法将一个参数化类型的对象作为参数并返回一个 int 值。 返回的 int 值 指示调用 compareTo() 方法的对象是否大于、等于或小于参数对象。

  • 返回正值(1 或更大)表示调用 compareTo() 的对象大于参数对象。
  • 返回零,表示两个对象相等。
  • 返回负值(-1 或更小)表示调用 compareTo() 方法的对象小于参数对象。

compareTo 使用示例

Integer 类实现了 Comparable 接口,因此可以调用 compareTo() 方法。

public class ComparableExample {

    public static void main(String[] args) {

        Integer valA = Integer.valueOf(45);
        Integer valB = Integer.valueOf(99);

        int comparisonA = valA.compareTo(valB);
        int comparisonB = valB.compareTo(valA);

        System.out.println(comparisonA);
        System.out.println(comparisonB);
    }
}

上面的例程,将输出 -1 和 1。因为 valA 是 45 小于 valB 的值99 所以 valA.compareTo(valB)返回 -1 ,同理 valB.compareTo(valA) 会返回 1。

编写 Comparable 的实现类

上面演示的都是 Java 内置类的比较,在项目开发的时候,针对我们自己定义 Class,如果需要,我们也可以通过让自定义 Class 实现 Comparable 接口,指定类实例之间的比较排序顺序规则。

public class Spaceship implements Comparable<Spaceship> {

    private String spaceshipClass = null;
    private String registrationNo = null;

    public Spaceship(String spaceshipClass, String registrationNo) {
        this.spaceshipClass = spaceshipClass;
        this.registrationNo = registrationNo;
    }

    @Override
    public int compareTo(Spaceship other) {
        int spaceshipClassComparison =
                this.spaceshipClass.compareTo(other.spaceshipClass);

        if(spaceshipClassComparison != 0) {
            return spaceshipClassComparison;
        }
        
        return this.registrationNo.compareTo(other.registrationNo);
    }
}    

上面这个例程,Spaceship 类的实例在比较时会先比较两个实例的 spaceshipClass 属性,如果相同则在比较两个实例的 registrationNo 。通过这种方式实现 compareTo() 方法,可以让实例基于多个因素进行比较。

Spaceship 类实现的是 Comparable<Spaceship>。通过在实现 Comparable 接口时指定泛型的类型参数,可以将 compareTo() 方法的参数从 Object 的实例更改为指定类型的实例。在上面的例程里,实现Comparable 接口时指定的类型参数是 Spaceship- 因此 compareTo 方法的参数类型也跟着变成 Spaceship

关于 Java 泛型方面的详细知识,可参考之前的文章:看了这篇Java 泛型通关指南,再也不怵满屏尖括号了

没有指定类型参数的 Comparable 实现,会有些不方便,compareTo 方法中进行比较时,需要先对参数进行强制类型转换,比如上面这个例程不指定类型参数的话,就会变成这样:

public class Spaceship implements Comparable {

    private String spaceshipClass = null;
    private String registrationNo = null;

    public Spaceship(String spaceshipClass, String registrationNo) {
        this.spaceshipClass = spaceshipClass;
        this.registrationNo = registrationNo;
    }

    @Override
    public int compareTo(Object o) {
        Spaceship other = (Spaceship) o;

        int spaceshipClassComparison =
                this.spaceshipClass.compareTo(other.spaceshipClass);

        if(spaceshipClassComparison != 0) {
            return spaceshipClassComparison;
        }

        return this.registrationNo.compareTo(other.registrationNo);
    }
}

注意,如果传入的参数为 null,compareTo() 方法会抛出 NullPointerException。所以我们不必在比较对象之前进行空检查。 类似地,如果传入的参数与 compareTo() 调用对象的类不属于同一类, compareTo() 方法会抛出 ClassCastException 。因此,不需要显式地进行类型检查,如果类不匹配,Java 会抛出ClassCastException。

传递比较特性

在实现 Java Comparable 接口时,Java 要求实现必须遵循以下传递比较特性:

如果对象 A大于对象B,对象 B 大于 对象 C,那么对象 A 也一定是大于对象 C 的。

Comparator 接口

Comparator (全限定名 java.util.Comparator) 接口于 Comparable 接口在名字看上去有点相似,主要是两个英文单词的拼写有些类似,容易混淆,其实两者表达的意思从命名上也能看出来,Comparable -- 形容词意思是可比较的,用于修饰对象;Comparator--名词意思是比较器,所以在记忆这两个接口时,可以借助英文里两个单词词性的不同,辅助我们记忆。

在 Java 里 Comparator 接口表示用于比较对象的外部比较器组件,而 Comparable 是由被比较对象本身实现的接口,表示对象本身是可以进行比较的。 类对象间通过 Comparable 实现的排序顺序在 Java 的术语惯例里被称为对象的自然排序顺序,而 Comparator 比较器的存在是为了让开发者能够在对象本身的自然排序外,根据自身需求自定义对象间的比较规则。

下面我们来看一下 Comparator 接口长什么样子。

Comparator 接口的定义

Comparator 接口,位于 java.util 包内,其定义如下:

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

Comparator 接口里只定义了一个 compare 方法,同样也是支持泛型的类型参数。Comparator 接口的 compare() 方法接收两个旨在通过 Comparator 比较器来进行比较的对象参数, 该方法会返回一个 int 类型的值,用于表示两个对象中哪个更大。

  • 返回正值意味着第一个对象大于第二个对象。
  • 返回负值意味着第一个对象小于第二个对象。
  • 返回 0 表示两个对象相等。

传递比较特性

在实现 Java Comparator 接口时,Java 要求实现必须遵循以下传递比较特性:

如果对象 A大于对象B,对象 B 大于 对象 C,那么对象 A 也一定是大于对象 C 的。

编写 Comparator 的实现

如果我们想要区别于类原有的自然排序顺序对实例进行排序,或者是对未实现 Comparable 接口的类的实例进行排序,那么我们可以通过让类实现 Comparator 接口来实现。

上面自定义 Comparable 接口的实现时, 我们为 Spaceship 类的实例指定了其自然排序顺序的规则,在这里我们在扩展一下,通过实现一个针对 Spaceship 类的排序器,自定义实例的排序规则。

比如,上面 Spaceship 类实现的 Comparable 实现里,基于实例的 spaceshipClass 和 registrationNo 两个属性依次进行了比较,完成实例的排序。现在假设我们只想根据 registrationNo 对 Spaceship 对象进行排序,而忽略 spaceshipClass 的比较。那么就可以通过实现上面的 Comparator 来完成。

import java.util.Comparator;

public class SpaceshipComparator implements Comparator<Spaceship> {

    @Override
    public int compare(Spaceship o1, Spaceship o2) {
        return o1.getRegistrationNo().compareTo(o2.getRegistrationNo());
    }
}

如果 Comparator 的实现需要比较被比较对象的数字型属性,一个简单的方法是简单地将两个数字相减并返回结果值。 举个例子,如果 Spaceship 类的 registrationNo 属性的类型是 int,那么可以像这样实现比较两个 Spaceship 对象的 registrationNo 的比较器。

public class SpaceshipComparator implements Comparator<Spaceship> {

    @Override
    public int compare(Spaceship o1, Spaceship o2) {
        return o1.getRegistrationNo() - o2.getRegistrationNo();
    }
}

项目中使用 Comparator 和 Comparable 的场景汇总

使用 Collections.sort 排序列表/集合

对 List 或者说 Collection 进行排序的时候,Collection 类族里有提供默认的 Collections.sort()排序方法,该方法使用的就是集合元素的类型对 Comparable 接口的实现逻辑来进行对象间相互,进而完成排序。比如:

List<String> list = new ArrayList<>();

list.add("c");
list.add("b");
list.add("a");

Collections.sort(list);

所以如果想按集合元素的自然排序规则对整个集合进行排序,直接使用Collections.sort()方法就好,如果集合内元素类型是我们自己定义的 Class ,那么就按照咱们上面编写介绍的Comparable 实现类的方式,给自定义 Class 加上 Comparable 实现即可。

如果我们不想给自定义 Class 加上 Comparable 实现逻辑,或者说想以非自然排序对集合元素进行排序的话,就得用到 Comparator 了,而Collections.sort()方法正好也有一个重载版的方法在第二个参数上接收一个指定的比较器,用比较器对集合元素进行排序。

使用比较器(Comparator)对列表进行排序

如果 List / Collection 中的对象没有实现 Comparable 接口,或者如果想以不同于对象的自然排序顺序对集合元素进行排序,那么就需要使用 Comparator 实现。

比如有下面一个自定义类:

public class Car{
    public String brand;
    public String numberPlate;
    public int noOfDoors;

    public Car(String brand, String numberPlate, int noOfDoors) {
        this.brand = brand;
        this.numberPlate = numberPlate;
        this.noOfDoors = noOfDoors;
    }
}

接下来我们自定义对列表里 Car 对象的排序规则

List<Car> list = new ArrayList<>();

list.add(new Car("Volvo V40" , "XYZ 201845", 5));
list.add(new Car("Citroen C1", "ABC 164521", 4));
list.add(new Car("Dodge Ram" , "KLM 845990", 2));

Comparator<Car> carBrandComparator = new Comparator<Car>() {
    @Override
    public int compare(Car car1, Car car2) {
        return car1.brand.compareTo(car2.brand);
    }
};

Collections.sort(list, carBrandComparator);

上面示例中的 Comparator 比较器的实现中,仅比较了 Car 对象的 brand 字段。我们还可以创建另一个比较器来比较车牌号,甚至是汽车门的数量。

Comparator 与 Stream 排序的结合使用

如果 Stream 操作用的熟练,使用 Stream 对集合元素进行排序,比使用Collections.sort方法更灵活,也更方便。

这点我们在 Stream 实用操作那篇文章给大家举过例子,比如用元素的某个字段为标准对集合进行正序、倒序排序:

// 以元素对象的personId 为标准对集合进行正序排序
personList.stream()
    .sorted(
        Comparator.comparing(Person::getPersonId)
    ).forEach(person -> System.out.println(person.getName()));
// 以元素对象的personId 为标准对集合进行倒序排序
personList.stream().sorted(
    Comparator.comparing(
        Person::getPersonId).reversed()
    ).forEach(person -> System.out.println(person.getName()));

甚至是实用元素的多个字段进行多维度排序,也很方便:

// 先按personId 排序,再按 age 排序
personList.stream().sorted(
    Comparator.comparing(Person::getPersonId)
    .thenComparing(Person::getAge)
).forEach(person -> System.out.println(person.getName()));

这部分操作在系列文章实用Stream操作汇总中有详细的解释和代码示例,大家使用时可以进行参考。

最后

Java 经常会被用到的常规内置接口就给大家梳理差不多了,下一篇文章,我们开始捋一捋 Java 在语言层面为支持函数编程提供的那些函数式接口。