通常要对数据进行排序,数据对象的类需要实现 Comparable 接口。有的情形需要在不修改类本身的情况下定义多种排序规则,则可以使用 Comparator 接口。所以这两种接口均用于排序,但使用方式略有不同。Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
一、Comparable 接口简介
Comparable 是可排序接口,它是一个泛型接口,可以指定比较的对象类型。
Comparable 接口定义如下:
package java.lang;
public interface Comparable<T> {
public int compareTo(T o);
}
接口定义中的T表示数据对象的类型,这里<T>表示泛型类型。通常T都是实现该接口的类本身。
函数接口只能有一个抽象方法,Comparable 接口的唯一抽象方法是比较器:compareTo(T o)。
如果一个类实现了Comparable接口,则需要实现compareTo方法。
比较规则:
假设我们通过“a.compareTo(b)”来比较a和b的大小。对象a与对象b比较,如果a小于b,则返回负整数;如果相等则返回0;如果a大于b,则返回正整数。
示例一:Student实现Comparable接口的完整示例
package function;
public class Student implements Comparable<Student> {
private String name;
private int age;
private int score;
public Student(String name, int age,int score) {
this.name = name;
this.age = age;
this.score = score;
}
public int getScore() {
return score;
}
@Override
public int compareTo(Student s) {
/***根据年龄比较***/
return this.age-s.age;
}
public static void main(String[] args) {
Student stu1=new Student("Alice",18,80);
Student stu2=new Student("Bob",20, 68);
System.out.println(stu1.compareTo(stu2));
}
}
根据不同属性可实现不同的比较器:
- 示例中的比较器是根据年龄比较:
@Override
public int compareTo(Student s) {
/***根据年龄比较***/
return this.age-s.age;
}
- 如果要根据成绩比较,则compareTo(Student s)方法要更新为如下所示:
@Override
public int compareTo(Student s) {
/***根据成绩比较***/
return this.score-s.score;
}
- 如果要根据姓名比较,则compareTo(Student s)方法要更新为如下所示:
@Override
public int compareTo(Student s) {
/***根据年龄比较***/
//return this.age-s.age;
/***根据成绩比较***/
//return this.score-s.score;
/***根据姓名比较***/
return this.name.compareTo(s.name);
}
因为,姓名的类型是String类,String类内部已经实现了compareTo方法,所以我们可以直接调用compareTo方法来实现姓名的比较。
若一个类实现了Comparable接口,就意味着“该类支持排序”。 实现后,在排序时这个类则可以按照compareTo定义的规则进行排序,无需额外指定比较器。
对于实现Comparable接口的类的对象的List列表(或数组)”,我们就可以通过 Collections.sort(或 Arrays.sort)来对该List列表(或数组)进行排序。
示例二:Student实现Comparable接口,用列表演示的完整示例
package function;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Student implements Comparable<Student> {
private String name;
private int age;
private int score;
public Student(String name, int age,int score) {
this.name = name;
this.age = age;
this.score = score;
}
public int getScore() {
return score;
}
@Override
public int compareTo(Student s) {
/***根据年龄比较***/
//return this.age-s.age;
/***根据成绩比较***/
//return this.score-s.score;
/***根据姓名比较***/
return this.name.compareTo(s.name);
}
@Override
public String toString() {
return "Student {姓名:"+name+" 年龄:"+age +" 成绩:"+score+"}";
}
public static void main(String[] args) {
Student stu1=new Student("Alice",18,80);
Student stu2=new Student("Bob",20, 68);
//System.out.println(stu1.compareTo(stu2));
List<Student> list = new ArrayList<>();
list.add(stu2);
list.add(stu1);
list.add(new Student("David",16, 85));
list.add(new Student("Charlie",19, 76));
System.out.println("排序前:");
list.forEach(System.out::println);
System.out.println("排序后:");
Collections.sort(list);
list.forEach(System.out::println);
}
}
测试结果图:
示例三:一个用冒泡排序进行排序的例程
冒泡排序用到的Student类的定义:
public class Student implements Comparable<Student> {
private String name;
private int age;
private int score;
public Student(String name, int age,int score) {
this.name = name;
this.age = age;
this.score = score;
}
public int getAge() {
return age;
}
public int getScore() {
return score;
}
public String getName() {
return name;
}
@Override
public int compareTo(Student s) {
/***根据姓名比较***/
return this.name.compareTo(s.name);
}
@Override
public String toString() {
return "Student {姓名:"+name+" 年龄:"+age +" 成绩:"+score+"}";
}
}
冒泡排序主程序:
public class BubbleSort {
public static void sortA(Comparable comparables[]){
for (int i = 0; i < comparables.length-1; i++) {
for(int j=0;j<comparables.length-1-i;j++){
if(comparables[j].compareTo(comparables[j+1])>0){
Comparable tmp=comparables[j];
comparables[j]=comparables[j+1];
comparables[j+1]=tmp;
}
}
}
}
public static void main(String[] args) {
Student[] students=new Student[]{
new Student("Bob",20, 68),
new Student("Alice",18,80),
new Student("David",16, 85),
new Student("Charlie",19, 76)
};
System.out.println("排序前");
for (int i = 0; i < students.length; i++) {
System.out.println(students[i]);
}
sortA(students);
System.out.println("排序后");
for (int i = 0; i < students.length; i++) {
System.out.println(students[i]);
}
}
}
测试效果图:
示例四:一个实现Comparable接口的电话号码,排序完整示例:
package function;
import java.util.Arrays;
public class Telephone implements Comparable<Telephone> {
private final int countryCode; //国家代码
private final String areaCode; //区号
private final int number; //电话号码
public Telephone(int countryCode, String areaCode, int number) {
this.countryCode = countryCode;
this.areaCode = areaCode;
this.number = number;
}
@Override
public int compareTo(Telephone o) {
int result = Integer.compare(countryCode, o.countryCode);
if (0 == result) {
result = String.CASE_INSENSITIVE_ORDER.compare(areaCode, o.areaCode);
if (0 == result) {
result = Integer.compare(number, o.number);
}
}
return result;
}
public static void main(String[] args) {
Telephone[] telephones = new Telephone[]{
new Telephone(86, "010", 86180412),
new Telephone(86, "010", 56279866),
new Telephone(86, "021", 68367160),
new Telephone(86, "0574", 86979122),
new Telephone(86, "0573", 68787826),
new Telephone(86, "021", 42965686)
};
// 数组排序
Arrays.sort(telephones);
// 打印排序后的数组元素
Arrays.stream(telephones).forEach(System.out::println);
}
@Override
public String toString() {
return "PhoneNumber{" +
"countryCode=" + countryCode +
", areaCode=" + areaCode +
", number=" + number +
'}';
}
}
测试效果图:
与排序相关的其他应用场景
当创建有序集合(如TreeSet)或有序映射(如TreeMap)时,如果类(例如Student类)未实现可排序接口,可指定排序比较器。例如,在创建树集TreeSet时可指定一个比较器定制排序。
下面的代码,假设Student类未实现可排序接口时,按分数排序时指定Lambda表达式作为排序比较器:
TreeSet<Student> set = new TreeSet<>( (a,b)->a.getScore()-b.getScore() );//λ表达式
当Student类实现了Comparable接口按分数排序时,则创建有序集合时就不需要指定排序器。下面的创建方式与上面是等价的。
TreeSet<Student> set = new TreeSet<>();
二、Comparator 接口简介
Comparator 是比较器接口,它是一个泛型接口,可以指定比较的对象类型。
我们若要对某个没有实现可排序接口(Comparable接口)的类进行排序;这时我们可以建立一个“该类的比较器”来进行排序。这个“比较器”只需要实现Comparator接口即可。
也就是说,我们可以通过“实现Comparator类来新建一个比较器”,然后通过该比较器对类进行排序。
实际上,Comparator接口的实现使用的是设计模式中的策略模式。
Comparator 接口的定义如下,有两个抽象方法:
package java.util;
public interface Comparator<T> {
int compare(T a, T b) ; //比较器
boolean equals(Object obj); //相等比较
}
说明: 函数接口只能有一个抽象方法。但是,Comparator接口却有二个抽象方法,其声明为函数接口,编译器也能通过其规范性检查,而且其实现类中不实现equals方法也没关系,是否令人疑惑不解呢?根据Java文档的解释,所有的类、接口都继承自超类Object类;如果接口中有抽象方法,是对超类Object类中public方法的重写,就不算真正的抽象方法。因此,Comparator接口中的抽象方法equals()是不算抽象方法的。
下面的示例是使用Lambda表达式来实现Comparator的比较器来排序:
//集合类的整型数的列表List,排序
List<Integer> numbers = Arrays.asList(6,12,4,9);
numbers.sort((a,b)->Integer.compare(a,b));
比较规则:
比较规则Comparator与Comparable接口是一致的。
假设我们通过“(a,b)->Integer.compare(a,b)”来比较a和b的大小。对象a与对象b比较,如果a小于b,则返回负整数;如果相等则返回0;如果a大于b,则返回正整数。
示例五:如果Student未实现Comparable接口,我们可以定义多个外部比较器,完整示例:
package function;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Student { //implements Comparable<Student> {
private String name;
private int age;
private int score;
public Student(String name, int age,int score) {
this.name = name;
this.age = age;
this.score = score;
}
public int getAge() {
return age;
}
public int getScore() {
return score;
}
public String getName() {
return name;
}
/***
@Override
public int compareTo(Student s) {
/***根据年龄比较***/
//return this.age-s.age;
/***根据成绩比较***/
//return this.score-s.score;
/***根据姓名比较***
return this.name.compareTo(s.name);
}
***/
@Override
public String toString() {
return "Student {姓名:"+name+" 年龄:"+age +" 成绩:"+score+"}";
}
public static void main(String[] args) {
Student stu1=new Student("Alice",18,80);
Student stu2=new Student("Bob",20, 68);
//System.out.println(stu1.compareTo(stu2));
List<Student> list = new ArrayList<>();
list.add(stu2);
list.add(stu1);
list.add(new Student("David",16, 85));
list.add(new Student("Charlie",19, 76));
System.out.println("排序前:");
list.forEach(System.out::println);
System.out.println("按姓名排序后:");
Collections.sort(list, new NameComparator());
list.forEach(System.out::println);
System.out.println("按年龄排序后:");
Collections.sort(list, new AgeComparator());
list.forEach(System.out::println);
System.out.println("按成绩排序后:");
Collections.sort(list, new ScoreComparator());
list.forEach(System.out::println);
}
}
class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return s1.getAge()- s2.getAge();
}
}
class ScoreComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return s1.getScore()-s2.getScore();
}
}
class NameComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return s1.getName().compareTo(s2.getName());
}
}
Comparator接口的常用方法
Comparator接口除了一个compare()抽象方法外,Comparator接口还提供了一些默认方法和静态方法,用于方便地创建和组合不同的比较器。这些方法主要有:
- reversed(): 返回一个与当前比较器相反顺序的比较器。
- thenComparing(Comparator<? super T> other): 返回一个先按照当前比较器进行比较,如果相等则按照other比较器进行比较的复合比较器。
- thenComparing(Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator): 返回一个先按照当前比较器进行比较,如果相等则按照keyExtractor提取出来的键值按照keyComparator进行比较的复合比较器。
- thenComparingInt(ToIntFunction<? super T> keyExtractor): 返回一个先按照当前比较器进行比较,如果相等则按照keyExtractor提取出来的int值进行比较的复合比较器。
- thenComparingLong(ToLongFunction<? super T> keyExtractor): 返回一个先按照当前比较器进行比较,如果相等则按照keyExtractor提取出来的long值进行比较的复合比较器。
- thenComparingDouble(ToDoubleFunction<? super T> keyExtractor): 返回一个先按照当前比较器进行比较,如果相等则按照keyExtractor提取出来的double值进行比较的复合比较器。
- naturalOrder(): 返回一个按照自然顺序进行比较的比较器,要求被比较的对象实现了Comparable接口。
- reverseOrder(): 返回一个按照自然顺序相反进行比较的比较器,要求被比较的对象实现了Comparable接口。
- comparing(Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator): 返回一个按照keyExtractor提取出来的键值按照keyComparator进行比较的比较器。
- comparing(Function<? super T, ? extends U> keyExtractor): 返回一个按照keyExtractor提取出来的键值按照自然顺序进行比较的比较器,要求键值实现了Comparable接口。
- comparingInt(ToIntFunction<? super T> keyExtractor): 返回一个按照keyExtractor提取出来的int值进行比较的比较器。
- comparingLong(ToLongFunction<? super T> keyExtractor): 返回一个按照keyExtractor提取出来的long值进行比较的比较器。
- comparingDouble(ToDoubleFunction<? super T> keyExtractor): 返回一个按照keyExtractor提取出来的double值进行比较的比较器。
- nullsFirst(Comparator<? super T> comparator): 返回一个将null值视为最小值,并使用comparator进行非null值比较的比较器。
- nullsLast(Comparator<? super T> comparator): 返回一个将null值视为最大值,并使用comparator进行非null值比较的比较器。
这些方法的引入增加了编程的灵活性,当我们利用Comparator接口编程时,可以更灵活地创建和组合不同的比较规则,而不需要每次都定义一个新的类。例如,我们想要对Person对象按照年龄从大到小排序,如果年龄相同则按照姓名从小到大排序,我们可以使用以下代码:
Arrays.sort(persons, Comparator.comparingInt(Person::getAge).reversed().thenComparing(Person::getName));
下面是一个更详细的应用实例
示例六:电话号码实现Comparator,外部比较器,电话号码排序完整实例
import java.util.Arrays;
import java.util.Comparator;
public class ComparatorTest {
public static void main(String[] args) {
Telephone[] telephones = new Telephone[]{
new Telephone(86, "010", 86180412),
new Telephone(86, "010", 56279866),
new Telephone(86, "021", 68367160),
new Telephone(86, "0574", 86979122),
new Telephone(86, "0573", 68787826),
new Telephone(86, "021", 42965686)
};
// 数组排序
Arrays.sort(telephones,new TelComparator());
// 打印排序后的数组元素
Arrays.stream(telephones).forEach(System.out::println);
}
}
class Telephone {
private final int countryCode; //国家代码
private final String areaCode; //区号
private final int number; //电话号码
public Telephone(int countryCode, String areaCode, int number) {
this.countryCode = countryCode;
this.areaCode = areaCode;
this.number = number;
}
public String getAreaCode() {
return areaCode;
}
public int getCountryCode() {
return countryCode;
}
public int getNumber() {
return number;
}
@Override
public String toString() {
return "PhoneNumber{" +
"countryCode=" + countryCode +
", areaCode=" + areaCode +
", number=" + number +
'}';
}
}
class TelComparator implements Comparator<Telephone> {
@Override
public int compare(Telephone o1, Telephone o2) {
return Comparator.comparingInt(Telephone::getCountryCode)
.thenComparing(Telephone::getAreaCode)
.thenComparingInt(Telephone::getNumber)
.compare(o1, o2);
}
}
测试效果图,与前文的一样:
三、Comparator 和 Comparable 比较
Comparable是可排序接口;若一个类实现了Comparable接口,就意味着“该类是可排序的”。
而Comparator是比较器接口;我们若要对某个类进行排序,而该类又没有实现Comparable可排序接口。则可以通过创建一个“该类的比较器”来进行排序。
Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
(一)应用场景的不同
- 适用于Comparable接口的场景:
当一个类的自然比较顺序是固定的,并且在整个应用程序中都适用时,使用 Comparable 是一个好的选择。例如,对于一些基本数据类型的包装类(如 Integer、Double 等),它们都实现了 Comparable 接口,具有自然的比较顺序。
如果一个类需要在集合中进行排序,并且希望使用默认的排序方式,那么实现 Comparable 接口可以方便地实现这个需求。例如,使用 Collections.sort(List list)方法对一个包含实现了 Comparable 接口的对象的列表进行排序。
- 适用于Comparator接口的场景
当需要根据不同的条件对同一个类的对象进行比较时,Comparator 非常有用。例如,对于一个 Student 类,可以根据姓名、年龄和成绩等不同的属性进行排序,通过定义不同的 Comparator 实现类来实现不同的比较策略。
在些情形可能无法修改被比较的类的源代码,这时可以使用 Comparator 来提供外部的比较方式。
Comparator 还可以用于临时改变对象的比较顺序,而不影响类的自然比较顺序。
(二)灵活性和可扩展性
- Comparable接口的灵活性和扩展性不佳,但代码更简洁更直观
通常实现 Comparable 接口的类比较逻辑固定在类内部。其比较方式是固定的,不会轻易改变。例如,基本数据类型的包装类(如 Integer、Double 等)。如果需要修改比较逻辑,就需要修改类的代码。
但是,由于 Comparable 是类的自然比较方式,它在一些场景下可以提供更简洁的代码和更直观的使用方式。
- Comparator接口的灵活性和扩展性更佳
Comparator 提供了更好的灵活性和可扩展性。可以根据不同的需求定义多个 Comparator 实现类,在不同的场景下使用不同的比较策略。
可以在程序中动态地选择不同的 Comparator 来实现不同的排序需求,而不需要修改被比较的类的代码。
(三)性能方面比较
- Comparable
由于 Comparable 的比较逻辑是在类内部实现的,因此在一些情况下可能会有更好的性能。例如,在使用 Collections.sort 方法对一个已经实现了 Comparable 接口的列表进行排序时,排序算法可以直接调用对象的 compareTo 方法,而不需要进行额外的方法调用。
- Comparator
使用 Comparator 进行比较可能会涉及到额外的方法调用和对象创建,因此在性能上可能会略逊于 Comparable。但是,在大多数情况下,这种性能差异是可以忽略不计的。
综上所述,Comparable 和 Comparator 在 Java 中都是用于实现对象比较的重要工具。它们在定义和实现方式、使用场景、灵活性和可扩展性以及性能等方面存在着一些区别。在实际编程中,应根据具体的需求选择合适的方式来实现对象的比较,以提高代码的可读性、可维护性和性能。
参考文献: