目录
Comparable接口简介
与前面Object的方法不同,compareTo方法并不是Object类的内置方法,而是Comparable接口唯一的可重写方法,该方法赋予特定类等同比较与顺序比较的功能。Comparable接口是一个泛型接口。
当一个类实现了 Comparable
接口,它就拥有了自然顺序,它就告诉 Java 运行时环境(JRE)如何比较该类的实例。这对于集合框架特别重要,因为它允许 JRE 根据元素的自然顺序自动对集合进行排序或维护元素的唯一性。如果未实现Comparable接口,类就默认没有自然顺序,就需要单独实现comparator接口来进行排序。
如何重写compareTo方法
compareTo的比较方法如下:
- 如果调用对象小于参数对象,则返回负整数。
- 如果调用对象等于参数对象,则返回零。
- 如果调用对象大于参数对象,则返回正整数。
需要遵循的重写原则
- 对称性:sgn(x.compareTo(y)) = -sgn(y.compareTo(x))必须成立;
- 传递性:x.compareTo(y)>0&&y.compareTo(z)>0,那么x.compareTo(z)>0必须成立;
- 同一性:如果x.compareTo(z)=0, 那么sgn(y.compareTo(x))=sgn(y.compareTo(z))必须成立。
除了以上三个原则,作者强烈建议将compareTo的实现与equals保持一致,即(x.compareTo(y)==0)==(x.equals(y)),要实现这个原则的关键就是要保持compareTo和equals方法中对比的值组件保持一致。
public class Book implements Comparable<Book> {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
@Override
public int compareTo(Book other) {
int titleComparison = this.title.compareTo(other.title);
if (titleComparison != 0) {
return titleComparison;
} else {
return this.author.compareTo(other.author);
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Book other = (Book) obj;
return title.equals(other.title) && author.equals(other.author);
}
比如上述这个案例,equals方法和compareTo方法都实现了对于title和author两个值组件的比较。但是作者后续又补充这个原则并不是必须的,换句话说compareTo在进行等同性比较的时候可以与equals不一致,这主要是因为compareTo和equals从作用上来说并不一样,compareTo返回的整数值是用于做排序的,而equals是用来做相等性判断的,换句话说,用作排序的值组件并不一定要和判断相等的一致,所以有时候从compareTo方法的性能优化和特定的设计场景(比如软件包的排序通过版本号判断,但是相等性通过标识符判断)上考虑,会有意让两者不一致。
但是要注意的是,两者的不一致会导致一系列的问题。尤其是在与一些顺序集合的交互过程中,比如在TreeSet中应用相关元素会导致基于compareTo的单一检测功能和单独元素的equals判断发生冲突。又比如在多线程环境中,不一致的比较可能导致竞态条件或死锁。所以,程序员应尽可能保证两个方法的一致性。
compareTo方法的实现方式
编写compareTo方法与编写equals方法非常相似,最底层都可以基于包装类静态的compare方法,但存在几个不同:
- comparable接口是泛型接口,因此compareTo方法只接受同类型的比较,而equals方法以Object类的实例为输入,具体操作时需要进行类型转换。
- 与equals方法不一样,comparable接口并不是所有类都实现了的,因此会存在对于某对象引用值组件进行比较的时候,对应类没有实现comparable接口的情况,这是就需要单独构建一个静态comparator构造器来实现比较。
通过Comparator比较器进行比较
可以通过comparator接口配置类的外接比较规则。它允许你在运行时动态地定义对象的排序规则,而不必修改类的实现。
比较器构建与应用步骤
1. 构建比较器类
首先需要创建一个实现了Comparator接口的类,由于Comparator接口也是泛型的,所以还需要指定接口类型。
import java.util.Comparator;
public class PersonComparator implements Comparator<Person> {
}
如果比较器后续不需要重复使用,可以在执行类的内部通过匿名内部类、Lambda表达式和方法引用等方式直接创建Comparator实例:
//匿名内部类
Comparator<Person> ageComparator = new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return Integer.compare(p1.getAge(), p2.getAge());
}
};
//Lambda表达式
Comparator<Person> ageComparator = (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge());
//方法引用
Comparator<Person> ageComparator = Comparator.comparingInt((Person pn)->pn.getAge());
作者重点介绍了静态方法引用的方式,comparingInt接受一个函数式接口(ToIntFunction)并返回一个 int
类型的值。后续比较可以继续调用thenComparingInt等Comparator上的实例方法。
comparator还实现了基于对象引用的静态方法comparing,它有两个重载,一个仅输入函数式接口,另一个还可以额外输入对象引用的其他比较器:
comparing(Function<T, U> keyExtractor)
comparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)
这两个方法都要求函数式接口返回一个实现了comparable接口的类型(U extends Comparable<? super U>),所以可以用于比较类的实例。
Comparator<Person> complexComparator = Comparator.comparingInt(Person::getAge)
.thenComparing(Person::toString);
这里就实现了先比较基础数据类型int,再去比较类实例String。如果对于String的比较有独立的构造器就可以使用第二种重载:
static Comparator<String> OtherOrder = new Comparator<String>(){
public int compare(String o1, String o2){
....
}
}
Comparator<Person> complexComparator = Comparator.comparingInt(Person::getAge)
.thenComparing(Person::toString,OtherOrder);