目录
一、从一个报错开始:为什么Card不能放进PriorityQueue?
[1.1 问题复现](#1.1 问题复现)
[1.2 Java中对象的比较方式概览](#1.2 Java中对象的比较方式概览)
[3.1 为什么需要覆写equals?](#3.1 为什么需要覆写equals?)
[3.2 如何正确覆写equals?](#3.2 如何正确覆写equals?)
[3.3 equals的局限性](#3.3 equals的局限性)
[4.1 Comparable是什么?](#4.1 Comparable是什么?)
[4.2 如何实现Comparable?](#4.2 如何实现Comparable?)
[4.3 使用Comparable](#4.3 使用Comparable)
[4.4 Comparable的优缺点](#4.4 Comparable的优缺点)
[5.1 Comparator是什么?](#5.1 Comparator是什么?)
[5.2 如何实现Comparator?](#5.2 如何实现Comparator?)
[5.3 使用Comparator](#5.3 使用Comparator)
[5.4 匿名内部类和Lambda简化写法](#5.4 匿名内部类和Lambda简化写法)
[5.5 Comparator的优缺点](#5.5 Comparator的优缺点)
[7.1 思路分析](#7.1 思路分析)
[7.2 代码实现](#7.2 代码实现)
[7.3 时间复杂度分析](#7.3 时间复杂度分析)
[8.1 核心要点回顾](#8.1 核心要点回顾)
[8.2 如何选择?](#8.2 如何选择?)
[8.3 学习建议](#8.3 学习建议)
写在前面:本文基于课程《回顾Java对象的比较》内容整理,结合个人学习笔记和实践经验编写。旨在帮助读者系统掌握Java中对象比较的三种方式。如需深入学习,建议配合Oracle官方文档和JDK源码阅读。
一、从一个报错开始:为什么Card不能放进PriorityQueue?
1.1 问题复现
上篇文章我们学习了优先级队列PriorityQueue,当时插入的都是Integer类型,一切正常。但如果我们尝试插入自定义类型呢?
class Card {
public int rank; // 牌面值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
}
public class TestPriorityQueue {
public static void main(String[] args) {
PriorityQueue<Card> pq = new PriorityQueue<>();
pq.offer(new Card(1, "♠")); // 抛出异常!
pq.offer(new Card(2, "♥"));
}
}
运行这段代码,你会得到一个异常:ClassCastException: Card cannot be cast to java.lang.Comparable。
为什么Integer可以,Card就不行?原因很简单:PriorityQueue底层是堆,堆在插入和删除时需要比较元素的大小。Integer实现了Comparable接口,知道自己该怎么比较;而Card没有告诉Java怎么比较两张牌的大小,于是程序就报错了。
1.2 Java中对象的比较方式概览
在Java中,比较两个对象有三种方式:
| 方式 | 关键字/方法 | 适用场景 |
|---|---|---|
| 1. 覆写equals | equals() |
判断两个对象是否"相等" |
| 2. 实现Comparable | compareTo() |
自然排序,类内部定义比较规则 |
| 3. 实现Comparator | compare() |
定制排序,类外部定义比较规则 |
这三种方式各有优劣,下面逐一深入讲解。
二、基本类型的比较:热身运动
在讲对象比较之前,先回顾一下基本类型的比较,这部分比较简单:
public class TestCompare {
public static void main(String[] args) {
// 整型比较
int a = 10;
int b = 20;
System.out.println(a > b); // false
System.out.println(a < b); // true
System.out.println(a == b); // false
// 字符比较(按ASCII码)
char c1 = 'A';
char c2 = 'B';
System.out.println(c1 > c2); // false (65 > 66?)
System.out.println(c1 < c2); // true
System.out.println(c1 == c2);// false
// 布尔比较(只能用==和!=)
boolean b1 = true;
boolean b2 = false;
System.out.println(b1 == b2); // false
System.out.println(b1 != b2); // true
}
}
基本类型直接用 >、<、==就可以比较,简单直接。但对象类型就没这么幸运了。
三、方式一:覆写equals方法
3.1 为什么需要覆写equals?
先看一个例子:
Card c1 = new Card(1, "♠");
Card c2 = new Card(1, "♠");
Card c3 = c1;
System.out.println(c1 == c2); // false,因为c1和c2指向不同对象
System.out.println(c1 == c3); // true,因为c1和c3指向同一对象
==比较的是引用地址,而不是对象的内容。在我们的认知中,两张都是"♠A"的牌应该是相等的,但Java不这么认为------除非你告诉它。
3.2 如何正确覆写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 obj) {
// 1. 如果是同一个对象,直接返回true
if (this == obj) {
return true;
}
// 2. 如果传入null,返回false
if (obj == null) {
return false;
}
// 3. 如果不是同一种类型,返回false
if (getClass() != obj.getClass()) {
return false;
}
// 4. 类型一致,比较内容
Card other = (Card) obj;
return this.rank == other.rank
&& this.suit.equals(other.suit);
}
}
覆写equals的标准套路:
-
自反性:和自己比,肯定相等
-
非空性:和null比,肯定不等
-
类型检查:必须是同类才能比较
-
内容比较:按照业务逻辑判断是否相等
小提示:实际开发中,IDEA可以自动生成equals方法(Alt+Insert → equals and hashCode),比自己手写更可靠。
3.3 equals的局限性
equals只能判断"相等"或"不相等",无法回答"谁大谁小"的问题。而优先级队列需要的是大小关系(大于、小于、等于),所以equals在这里派不上用场。
四、方式二:实现Comparable接口
4.1 Comparable是什么?
Comparable是java.lang包中的一个泛型接口,定义如下:
public interface Comparable<T> {
/**
* 比较当前对象和参数对象
* @return 负数:当前对象小于参数对象
* 正数:当前对象大于参数对象
* 零: 两个对象相等
*/
public int compareTo(T o);
}
4.2 如何实现Comparable?
让Card类实现Comparable接口,并重写compareTo方法:
public class Card implements Comparable<Card> {
public int rank;
public String suit;
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
public int compareTo(Card other) {
// 先按牌面值比较
if (this.rank != other.rank) {
return this.rank - other.rank;
}
// 牌面值相同,按花色比较
return this.suit.compareTo(other.suit);
}
@Override
public String toString() {
return suit + " " + rank;
}
}
4.3 使用Comparable
public class TestComparable {
public static void main(String[] args) {
Card p = new Card(1, "♠");
Card q = new Card(2, "♠");
Card o = new Card(1, "♠");
System.out.println(p.compareTo(o)); // 0,相等
System.out.println(p.compareTo(q)); // 负数,p < q
System.out.println(q.compareTo(p)); // 正数,q > p
// 现在可以放入PriorityQueue了!
PriorityQueue<Card> pq = new PriorityQueue<>();
pq.offer(new Card(3, "♥"));
pq.offer(new Card(1, "♠"));
pq.offer(new Card(2, "♣"));
System.out.println(pq.poll()); // ♠ 1(最小值优先)
System.out.println(pq.poll()); // ♣ 2
System.out.println(pq.poll()); // ♥ 3
}
}
4.4 Comparable的优缺点
优点:
-
实现简单,类内部定义比较规则
-
一旦实现,该类的所有实例都可以直接比较
缺点:
-
侵入性强:需要修改类的源代码
-
不够灵活:一个类只能有一种比较规则。比如Card有时想按牌面值比,有时想按花色比,Comparable就无法满足
五、方式三:使用Comparator比较器
5.1 Comparator是什么?
Comparator是java.util包中的一个泛型接口:
@FunctionalInterface
public interface Comparator<T> {
/**
* 比较两个参数对象
* @return 负数:o1 < o2
* 正数:o1 > o2
* 零: o1 == o2
*/
int compare(T o1, T o2);
}
注意区分:Comparable是"我比较我自己",Comparator是"第三方比较两个对象"。
5.2 如何实现Comparator?
import java.util.Comparator;
// 定义一个比较器类,按牌面值比较
class CardRankComparator implements Comparator<Card> {
@Override
public int compare(Card o1, Card o2) {
// 处理null的情况
if (o1 == o2) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
return o1.rank - o2.rank;
}
}
// 再定义一个比较器,按花色比较
class CardSuitComparator implements Comparator<Card> {
@Override
public int compare(Card o1, Card o2) {
if (o1 == o2) return 0;
if (o1 == null) return -1;
if (o2 == null) return 1;
return o1.suit.compareTo(o2.suit);
}
}
5.3 使用Comparator
public class TestComparator {
public static void main(String[] args) {
Card p = new Card(1, "♠");
Card q = new Card(2, "♥");
Card o = new Card(1, "♠");
// 创建比较器对象
CardRankComparator rankCmp = new CardRankComparator();
CardSuitComparator suitCmp = new CardSuitComparator();
// 按牌面值比较
System.out.println(rankCmp.compare(p, o)); // 0
System.out.println(rankCmp.compare(p, q)); // 负数
// 按花色比较
System.out.println(suitCmp.compare(p, q)); // ♠ > ♥? 正数
// 使用比较器创建PriorityQueue
PriorityQueue<Card> pqByRank = new PriorityQueue<>(rankCmp);
pqByRank.offer(new Card(3, "♥"));
pqByRank.offer(new Card(1, "♠"));
pqByRank.offer(new Card(2, "♣"));
System.out.println(pqByRank.poll()); // ♠ 1
}
}
5.4 匿名内部类和Lambda简化写法
Comparator接口只有一个抽象方法,可以用Lambda表达式简化:
// 匿名内部类方式
PriorityQueue<Card> pq1 = new PriorityQueue<>(new Comparator<Card>() {
@Override
public int compare(Card o1, Card o2) {
return o1.rank - o2.rank;
}
});
// Lambda表达式方式(更简洁)
PriorityQueue<Card> pq2 = new PriorityQueue<>(
(o1, o2) -> o1.rank - o2.rank
);
// 方法引用方式(最简洁)
PriorityQueue<Card> pq3 = new PriorityQueue<>(
Comparator.comparingInt(c -> c.rank)
);
5.5 Comparator的优缺点
优点:
-
零侵入:不需要修改Card类的源代码
-
灵活多变:可以为同一个类定义多种比较规则
-
解耦:比较逻辑和业务逻辑分离
缺点:
- 需要额外定义比较器类,代码量稍多
六、三种方式对比总结
| 对比维度 | equals | Comparable | Comparator |
|---|---|---|---|
| 所属包 | java.lang.Object | java.lang | java.util |
| 方法名 | equals(Object) | compareTo(T) | compare(T, T) |
| 返回值 | boolean | int(负/零/正) | int(负/零/正) |
| 比较能力 | 只能判断相等 | 可判断大小 | 可判断大小 |
| 侵入性 | 中(覆写Object方法) | 强(修改类定义) | 弱(无需修改类) |
| 灵活性 | 低 | 低(一种规则) | 高(多种规则) |
| 适用场景 | 判断对象是否相等 | 自然排序 | 定制排序 |
七、实战:用Comparator创建大小堆解决TopK问题
把学到的知识用在实际场景中------求数组中最小的K个数。
7.1 思路分析
求最小的K个数,需要一个大根堆:
-
用前K个元素建大根堆
-
遍历剩余元素,如果比堆顶小,替换堆顶并调整
-
遍历结束后,堆中的K个元素就是最小的K个
**为什么找最小要用大根堆?** 因为大根堆的堆顶是堆中最大的元素。新元素如果比堆顶小,说明它比当前候选者中最大的还要小,可以入选。
7.2 代码实现
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Arrays;
public class TopKExample {
// 大根堆比较器
static class GreaterIntComp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1; // 降序,大的优先
}
}
// 小根堆比较器
static class LessIntComp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2; // 升序,小的优先
}
}
// 求最小的K个数
public static int[] smallestK(int[] array, int k) {
if (k <= 0) {
return new int[0];
}
// 1. 创建大根堆
GreaterIntComp cmp = new GreaterIntComp();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(cmp);
// 2. 用前K个元素建堆
for (int i = 0; i < k; i++) {
maxHeap.offer(array[i]);
}
// 3. 遍历剩余元素
for (int i = k; i < array.length; i++) {
int top = maxHeap.peek();
if (array[i] < top) {
maxHeap.poll();
maxHeap.offer(array[i]);
}
}
// 4. 收集结果
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = maxHeap.poll();
}
return result;
}
// 求最大的K个数
public static int[] greatestK(int[] array, int k) {
if (k <= 0) {
return new int[0];
}
// 找最大用最小堆
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int i = 0; i < k; i++) {
minHeap.offer(array[i]);
}
for (int i = k; i < array.length; i++) {
int top = minHeap.peek();
if (array[i] > top) {
minHeap.poll();
minHeap.offer(array[i]);
}
}
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = minHeap.poll();
}
return result;
}
public static void main(String[] args) {
int[] array = {4, 1, 9, 2, 8, 0, 7, 3, 6, 5};
int[] smallest = smallestK(array, 3);
System.out.println("最小的3个数:" + Arrays.toString(smallest));
// 输出:[0, 1, 2](顺序可能不同)
int[] greatest = greatestK(array, 3);
System.out.println("最大的3个数:" + Arrays.toString(greatest));
// 输出:[9, 8, 7](顺序可能不同)
}
}
7.3 时间复杂度分析
-
建堆:O(k)
-
遍历剩余N-K个元素,每次可能触发调整:O((N-K) log k)
-
总体:O(N log k)
当k远小于N时,这个方法比全排序(O(N log N))高效得多。
八、总结与学习建议
8.1 核心要点回顾
-
基本类型 直接用
>、<、==比较 -
对象比较有三种方式:
-
equals:判断相等,不能比大小 -
Comparable:自然排序,侵入性强,一种规则 -
Comparator:定制排序,零侵入,多种规则
-
-
PriorityQueue要求元素可比,要么实现Comparable,要么传入Comparator
-
TopK问题:找最小K个用大根堆,找最大K个用小根堆
8.2 如何选择?
| 场景 | 推荐方式 |
|---|---|
| 判断两个对象是否相等 | 覆写equals |
| 类有天然的排序规则(如按年龄排序学生) | 实现Comparable |
| 需要多种排序规则 | 定义多个Comparator |
| 无法修改类的源码 | 使用Comparator |
| PriorityQueue需要自定义排序 | 传入Comparator |
8.3 学习建议
-
多写多练:自己定义几个类,分别用三种方式实现比较
-
阅读源码:看看Integer、String等类是如何实现Comparable的
-
注意边界 :比较时注意null的处理、溢出问题(
o1 - o2可能溢出,建议用Integer.compare(o1, o2))
如果你觉得这篇文章对你有帮助,欢迎点赞收藏。下一篇我们将继续探索Java集合框架的更多精彩内容,敬请期待!
注:本文为个人学习总结,所有代码示例均为独立编写。建议读者在学习过程中结合JDK官方文档和源码进行验证。