前言
博主在很早之前写过一篇面向对象编程的文章,其中粗略的介绍了一下Comparable接口的使用,现在问题来了,Comparabe接口和Comparator接口的异同点是什么呢?
一、元素的比较
1.1 基本类型的比较
在Java中,基本类型的对象可以直接比较大小。
1.2 对象的比较
Java中引用类型的变量不能直接按照 > 或者 < 方式进行比较。
那为什么==可以比较?
因为:对于用户实现自定义类型,都默认继承自Object类,而Object类中提供了equals方法,而==默认情况下调用的就是equals方法,但是该方法的比较规则是:没有比较引用变量引用对象的内容,而是直接比较引用变量的地址,但有些情况下该种比较就不符合题意。
//Object中equal的实现,可以看到:直接比较的是两个引用变量的地址
public boolean equals(Object obj) {
return (this == obj);
}
二、引用对象的比较
有些情况下,需要比较的是对象中的内容,比如:向优先级队列中插入某个对象时,需要对按照对象中内容来调整堆,那该如何处理呢?
2.1 覆写基类的equals
public class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
public boolean equals(Object o) {
// 自己和自己比较
if (this == o) {
return true;
}
// o如果是null对象,或者o不是Card的子类
if (o == null || !(o instanceof Card)) {
return false;
}
// 注意基本类型可以直接比较,但引用类型最好调用其equal方法
Card c = (Card) o;
return rank == c.rank && suit.equals(c.suit);
}
}
注意: 一般覆写 equals 的套路就是上面演示的
- 如果指向同一个对象,返回 true
- 如果传入的为 null,返回 false
- 如果传入的对象类型不是 Card,返回 false
- 按照类的实现目标完成比较,例如这里只要花色和数值一样,就认为是相同的牌
- 注意下调用其他引用类型的比较也需要 equals,例如这里的 suit 的比较
- 覆写基类equal的方式虽然可以比较,但缺陷是:equal只能按照相等进行比较,不能按照大于、小于的方式进行比较。
2.2 Comparable接口
接下来将用一个例子介绍java.lang.Comparable接口:
给对象数组排序
给定一个学生类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序).
Student[] students = new Student[] {
new Student("张三", 95),
new Student("李四", 96),
new Student("王五", 97),
new Student("赵六", 92),
}
按照我们之前的理解, 数组工具类我们有一个现成的 sort 方法, 能否直接使用这个方法呢?
Arrays.sort(students);
System.out.println(Arrays.toString(students));
// 运行出错, 抛出异常.
Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to
java.lang.Comparable
仔细思考, 不难发现, 和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确. 而两个学生对象的大小关系怎么确定? 需要我们额外指定!
让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法:
class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
@Override
public int compareTo(Object o) {
if(this == 0){
return 0; //返回0表示相等
}
if(o instanceof Student){
//当前传入的o就是Student类型的引用,向下转型还原为Student
//要比较Student对象的大小关系,就要用到Student的独有属性,向下转型
Student stu = (Student)o;
return this.score - stu.score;
}
//若传入不是学生类,则抛出异常
throw new IllegalArgumentException("不是学生类型,无法比较!")
}
在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.
然后比较当前对象和参数对象的大小关系(按分数来算).
- 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
- 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
- 如果当前对象和参数对象不分先后, 返回 0;
再次执行程序, 结果就符合预期了
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
注意事项:
对于 sort 方法来说, 需要传入的数组的每个对象都是 "可比较" 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则。
为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序):(其实Arrays.sort()内部也是和下面代码类似的,只是被封装了)
public static void sort(Comparable[] array) {
for (int bound = 0; bound < array.length; bound++) {
for (int cur = array.length - 1; cur > bound; cur--) {
if (array[cur - 1].compareTo(array[cur]) > 0) {
// 说明顺序不符合要求, 交换两个变量的位置
Comparable tmp = array[cur - 1];
array[cur - 1] = array[cur];
array[cur] = tmp;
}
}
}
}
再次执行代码
sort(students);
System.out.println(Arrays.toString(students));
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
2.3 Comparator接口
用户自定义比较器类,实现Comparator接口:
Public interface Comparator<T> {
// 返回值:
// < 0: 表示 o1 指向的对象小于 o2 指向的对象
// == 0: 表示 o1 指向的对象等于 o2 指向的对象
// > 0: 表示 o1 指向的对象等于 o2 指向的对象
int compare(T o1, T o2);
}
假设现在覆写了Comparable接口的compareTo方法:
- 如果需要得到一个升序数组 => this - o
- 要得到一个降序数组,又得改代码 => o-this
假设如今业务需要要更改成逆序的,又得更改CompareTo方法。
而程序开发中遵循一个开闭原则:
**一段程序应该对扩展开放,对修改关闭。
写好的代码别老改,要有新场景,就拓展新代码,不要影响老代码。**
==所以我们可以使用Comparator接口:==
一个类若实现了这个接口,表示这个类天生就是为别的类的大小关系服务的:
StudentSec这个类天生就是为了Student对象的排序而存在
覆写compare方法(o1,o2) => int
**表示o1和o2的大小关系:
返回值 >0 :o1> o2
返回值 =0 :o1 = o2
返回值<0 :o < o2**
当把Student类的大小关系比较从Student类中“解耦”,此时的比较策略非常灵活,需要哪种方式,只需要创新一个新的类实现Comparator接口即可,根据此时大小关系的需要传入比较器的对象。(策略模式)
三、Comparable和Comparator的异同
3.1 共同点
- 这两个接口一般都是用来实现集合内的排序(如Collections.sort() 、Arrays.sort()、List.sort(Comparator))。
以下是Collections类下的sort方法,可以传入继承Comparable接口的泛型对象,也可以传入当前list 和 一个comparator比较器。
- 当调用集合排序方法的时候,如果直接传入list就会自行调用对象的compareTo()方法来实现对象的比较。如果传入list和coparator的话,就会调用compare()方法
3.2 区别
- Comparable接口位于java.lang包下,Comparator位于java.util包下
- Comparable接口只提供了一个compareTo()方法;Comparator接口不仅提供了compare()方法【两个参数】,还提供了其他默认方法,如reversed()、thenComparing(),使我们可以按照更多的方式进行排序
- Comparator相对于Comparable来说更加的灵活,耦合度低。
- 如果要用Comparable接口,则必须实现这个接口,并重写compareTo()方法【一个参数】;但是Comparator接口可以在类外部使用,通过将该接口的一个匿名类对象当做参数传递给Collections.sort()方法或者Arrays.sort()方法实现排序。
- Comparator体现了一种策略模式,即可以不用要把比较方法嵌入到类中,而是可以单独在类外部使用,这样我们就可有不用改变类本身的代码而实现对类对象进行排序。
总结
用简单的话来讲就是:
- Comparable接口可以让当前这个类拥有可以比较的能力,就可以和类型的对象进行比较
- Comparator接口可以让我们定义不同的类,然后我们可以用这些自己定义好的排序类去对list中的对象按照某种规则去排序,不想用的时候可以换另一种排序类。(比如升序和降序)