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 接口对数组集合/数组进行排序,需要两个条件
- 实例类( 例如 Person )要实现 Comparable 接口,用来定义排序规则。
- 使用工具类对集合/数组进行排序。集合类使用
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 对其进行排序
- 使用工具类的
Collections.sort(List, Comparator)
。 - 使用 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()
方法,并且保持 Comparator
和 equals()
方法,在定义数据相等上,保持一致。因此,完整的 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 写出的代码更具有观赏性。