Java Comparable与Comparator

Comparable接口用于定义对象的排序规则,Comparator则提供灵活的外部排序策略。文章通过示例详细阐述了如何使用Comparable和Comparator对集合和数组进行排序,并强调了两者在数据相等性和排序规则一致性上的注意事项。此外,还介绍了Java8中Comparator的新特性,增强了代码的可读性和灵活性。
摘要由CSDN通过智能技术生成

Comparable

Comparable 接口主要是用来定义排序的规则,然后再使用相关的排序方法对集合进行排序。这样说有点抽象,我们通过一个例子来解释。

假如现在需要对一个 Person 对象集合进行排序,那么首先让 Person 类实现 Comparable 接口,这样就可以定义一个排序的规则。代码如下

public class Person implements Comparable<Person>{
    public final String name;
    public final int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person o) {
        if (name.compareTo(o.name) == 0) {
            // 如果名字相同,就按年龄排序
            return Integer.compare(age, o.age);
        } else {
            // 默认按名字排序
            return name.compareTo(o.name);
        }
    }

    @NonNull
    @Override
    public String toString() {
        return "Person: name = " + name + ", age = " + age;
    }
}

Person 类实现了 Comparable 接口,在 compareTo() 方法中定义了排序的规则,这个规则就是首先按照名字进行排序,如果名字相同,再按照年龄进行排序。

排序规则已经定义好了,现在让我们创建一个 List 集合,保存 Person 数据,代码如下

List<Person> personList = new ArrayList<>();
personList.add(new Person("david", 12));
personList.add(new Person("chow", 18));
personList.add(new Person("boy", 8));
personList.add(new Person("Chow", 28));

现在排序规则和集合已经都有了,现在需要使用集合工具类进行排序,代码如下

Collections.sort(personList);

如果你打印这个集合,可以看到如下输出

[
Person: name = boy, age = 8, 
Person: name = chow, age = 18, 
Person: name = chow, age = 28, 
Person: name = david, age = 12
]

从这个输出可以看出,集合中的元素首先是按名字排序,然后再按年龄排序的。

其实 Person 类对象也可以保存到数组中,并且可以对数组进行排序,代码如下

Person[] personArray = {new Person("david", 12),
                new Person("chow", 28),
                new Person("boy", 8),
                new Person("chow", 18)};
Arrays.sort(personArray);

打印这个数组,会看到与前面一样的结果。

从这两个例子中可以看出,如果要使用 Comparable 接口对数组集合/数组进行排序,需要两个条件

  1. 实例类( 例如 Person )要实现 Comparable 接口,用来定义排序规则。
  2. 使用工具类对集合/数组进行排序。集合类使用 Collections.sort(),而数组使用 Arrays.sort()

另外,实现了 Comparable 接口的类,可以作为 SortedMap 接口的 key 值,也可以作为 SortedSet 接口的元素。这两个接口的常用实现类分别为 TreeMap 和 TreeSet。

你以为这样就完了吗?作为一个敏感的开发者,如果自定义的实例类需要保存到集合中,还需要实现 equals()hashCode() 方法,我们可以通过 IDE 快速生成这些方法的实现。因此上面的 Person 类完整的代码如下

public class Person implements Comparable<Person>{
    public final String name;
    public final int age;
    public Person(String name, int age)
    {
    	Objects.requireNonNull(name);
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person o) {
        if (name.compareTo(o.name) == 0) {
            // 如果名字相同,就按年龄排序
            return Integer.compare(age, o.age);
        } else {
            // 默认按名字排序
            return name.compareTo(o.name);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                name.equals(person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @NonNull
    @Override
    public String toString() {
        return "Person: name = " + name + ", age = " + age;
    }
}

在这里我们还要注意,Comparable 接口的 compareTo() 方法定义数据相等的方式,最好要与 equals() 方法一致。注意,我这里的用词是最好,并一定要强制一致,因为在某些特殊情况下是允许不一致的,但是最好通过注释说明原因。

我们这里举例来说明不一致的情况,例如在 compareTo() 方法中,比较名字忽略大小写,而在 equals() 中比较名字对大小写敏感,那么就会出现不一致的情况,从而会导致在某些集合中表现出奇怪的现象。

Comparator

Comparable 其实可以看作在实体类的内部定义的排序规则,这是一个死的规则,而有时候我们需要对集合/数组进行不同的排序,例如我们需要反向排序,那这个时候就比较麻烦了,我们需要修改实例类排序规则,例如直接修改实体类源码,但是有时候我们并没有这个权限。

设计模式的开闭原则,要求对修改是封闭的,对扩展是开放的。我们为何不能对排序排序规则进行对外扩展呢?Comparator接口正好满足这种设计模式,它定义了排序规则,集合/数组可以使用这个接口对象来排序,并且不需要实例类实现 Comparable 接口。

说了这么多,我们通过一个例子来解释下。现在定义一个 Student 实例类

public class Student{
    private final String name;
    private final int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    @NonNull
    @Override
    public String toString() {
        return "Student: name = " + name + ", score = " + score;
    }
}

注意,这个 Student 类不需要实现 Comparable 接口,因为接下来我们使用 Comparator 接口来对集合或数组进行排序。

现在让我们来创建一个 List 集合,并保存数据

List<Student> studentList = new ArrayList<>();
studentList.add(new Student("Stephen", 88));
studentList.add(new Student("Frank", 90));
studentList.add(new Student("Chow", 88));
studentList.add(new Student("David", 80));

对于 List 集合,可以有两种方式使用 Comparator 对其进行排序

  1. 使用工具类的 Collections.sort(List, Comparator)
  2. 使用 List 接口的 List.sort(Comparator)

List.sort(Comparator) 是 Java 8 添加的 default 方法,由于 Java 保持向后兼容性,因此在以前是无法扩展 List 接口的,而 Java 8 的 default 方法既保持了向后兼容,又可以扩展 List 接口。

现在我们使用 List 接口对数据进行排序,代码如下

studentList.sort(Comparator.comparing(Student::getScore)
						.thenComparing(Student::getName)
						.reversed());

这里我们使用了 Java 8 的特性,创建一个 Comparator 对象,它定义的排序规则是先按分数进行排序,如果分数相同,再按名字进行排序,最后对整体进行返向排序。

Java 8 的对 Comparator 进行了很大的改动,如果读者对这个排序规则的写法很疑惑,请深入学习 Java 8 的知识。

如果打印这个集合,会看到如下输出

[
Student: name = Frank, score = 90, 
Student: name = Stephen, score = 88, 
Student: name = Panda, score = 88, 
Student: name = David, score = 80
]

从这个输出可以看出,整体是按分数的降序排列,如果分数相同,再按名字的降序排列。

如果是数组,还是需要使用工具类的 Arrays.sort()方法,并且传入一个 Comparator 对象,即可排序。

从这个例子可以看出,通过 Comparator 把排序规则在外部进行定义,这相当于一个策略模式,因为我们可以随时定义其它的排序规则。并且如果使用 Comparator 进行排序,那么就不需要实例类实现 Comparable 接口。

那么对于 SortedMap 接口和 SortedSet 接口保存 Student 类对象,是否也不需要 Student 类实现 Comparable 接口呢?这个答案是肯定的,只不过在创建 SortedMap 和 SortedSet 实现类对象时,需要传入一个 Comparator 对象。代码如下

// TreeSet 为 SortedSet 的实现类
TreeSet<Student> treeSet = new TreeSet<>(Comparator.comparingInt(Student::getScore)
                .thenComparing(Student::getName));

这个传入的 Comparator 一经传入,是不可修改的,因此排序规则定死了。如果你想使用另外的排序规则,那么可以重新创建一个 TreeSet ,复制前一个 TreeSet 数据,并传入一个新的 Comparator 即可。SortedMap 接口也是一样的。

另外,正如前面所说,如果自定义的数据类,要保存到集合中,还需要定义 equals()hashCode() 方法,并且保持 Comparatorequals() 方法,在定义数据相等上,保持一致。因此,完整的 Student 类写法如下

public class Student{
    private final String name;
    private final int score;

    public Student(String name, int score) {
        Objects.requireNonNull(name);
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return score == student.score &&
                name.equals(student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, score);
    }

    @NonNull
    @Override
    public String toString() {
        return "Student: name = " + name + ", score = " + score;
    }
}

总结

从本文可以看出,使用 Comparator 接口应该是优于 Comparable 接口,因为它可以根据不同的情况创建不同的排序规则,这具有非常高的灵活性。再配合 Java 8 的特性,使用 Comparator 写出的代码更具有观赏性。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值