前言
在 Java 编程中,我们经常需要对对象进行比较操作。对于基本数据类型,可以直接使用 >、<、== 等运算符进行比较,但对于对象,情况则复杂得多。特别是在使用某些集合框架时(如 PriorityQueue),对象之间的比较能力成为必须掌握的技能。
考虑这样一个场景:我们需要管理一副扑克牌,并按照牌面大小进行排序或优先级处理。如何让 Java 理解"红桃 A 比黑桃 K 大"这样的业务逻辑?这正是对象比较要解决的核心问题。
一、基本类型 vs 引用类型的比较
1.1 基本类型的直接比较
Java 的基本类型(int、char、boolean 等)可以直接使用关系运算符进行比较:
int a = 10, b = 20;
System.out.println(a > b);
System.out.println(a < b);
System.out.println(a == b);
char c1 = 'A', c2 = 'B';
System.out.println(c1 < c2);
boolean b1 = true, b2 = false;
System.out.println(b1 == b2);
1.2 引用类型比较的困境
对于自定义的引用类型,直接使用关系运算符会导致编译错误:
class Card {
public int rank;
public String suit;
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
}
public class Test {
public static void main(String[] args) {
Card c1 = new Card(1, "♠");
Card c2 = new Card(2, "♥");
System.out.println(c1 == c2);
}
}
关键点:== 对于引用类型比较的是内存地址,而不是对象内容。即使两个 Card 对象的 rank 和 suit 完全相同,== 也会返回 false。
二、对象比较的三种核心方式
2.1 方式一:覆写基类的 equals 方法
2.1.1 equals 方法的本质
所有 Java 类都隐式继承自 Object 类,Object 类中的 equals 方法默认实现如下:
public boolean equals(Object obj) {
return (this == obj);
}
2.1.2 如何正确覆写 equals
要比较对象内容,必须在自定义类中覆写 equals 方法:
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;
if (o == null || getClass() != o.getClass()) return false;
Card card = (Card) o;
return rank == card.rank && suit.equals(card.suit);
}
@Override
public int hashCode() {
return Objects.hash(rank, suit);
}
}
2.1.3 equals 方法的标准实现模式
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
- 类型转换
- 逐个比较关键字段
局限性:equals 只能判断"相等与否",无法判断"大小关系"。
2.2 方式二:实现 Comparable 接口(自然排序)
2.2.1 Comparable 接口定义
public interface Comparable<T> {
int compareTo(T o);
}
2.2.2 实现 Comparable 接口
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 o) {
if (o == null) {
return 1;
}
if (this.rank != o.rank) {
return this.rank - o.rank;
}
return this.suit.compareTo(o.suit);
}
}
2.2.3 使用示例
public class Test {
public static void main(String[] args) {
Card card1 = new Card(5, "♠");
Card card2 = new Card(3, "♥");
Card card3 = new Card(5, "♣");
System.out.println(card1.compareTo(card2));
System.out.println(card1.compareTo(card3));
System.out.println(card2.compareTo(card1));
}
}
特点:Comparable 是"内部比较器",定义了对象的自然排序顺序。
2.3 方式三:实现 Comparator 接口(定制排序)
2.3.1 Comparator 接口定义
public interface Comparator<T> {
int compare(T o1, T o2);
}
2.3.2 创建比较器类
import java.util.Comparator;
class RankDescComparator 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 o2.rank - o1.rank;
}
}
class SuitFirstComparator 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;
int suitCompare = o1.suit.compareTo(o2.suit);
if (suitCompare != ) {
suitCompare;
}
o1.rank - o2.rank;
}
}
2.3.3 使用示例
public class Test {
public static void main(String[] args) {
Card c1 = new Card(5, "♠");
Card c2 = new Card(3, "♥");
Card c3 = new Card(5, "♣");
Comparator<Card> rankDescComp = new RankDescComparator();
Comparator<Card> suitFirstComp = new SuitFirstComparator();
System.out.println(rankDescComp.compare(c1, c2));
System.out.println(suitFirstComp.compare(c1, c3));
}
}
特点:Comparator 是"外部比较器",可以为同一个类定义多种排序规则。
2.4 三种方式对比分析
| 比较方式 | 所属包 | 方法名 | 特点 | 使用场景 |
|---|
| equals | java.lang.Object | equals() | 只能比较相等性,不能比较大小 | 判断两个对象是否逻辑相等 |
| Comparable | java.lang | compareTo() | 内部比较器,定义自然顺序 | 类有明确的默认排序规则 |
| Comparator | java.util | compare() | 外部比较器,灵活定义多种规则 | 需要多种排序方式,或无法修改类源码 |
设计原则:
- 如果类有自然排序(如整数、字符串),实现 Comparable
- 如果需要多种排序方式,或无法修改类源代码,使用 Comparator
- equals 用于判断逻辑相等,常与 hashCode 一起覆写
三、PriorityQueue 中的对象比较机制
3.1 PriorityQueue 对比较的要求
PriorityQueue(优先级队列)底层基于堆实现,要求所有元素必须能够比较大小。当插入自定义对象时,必须提供比较方式。
PriorityQueue<Card> queue = new PriorityQueue<>();
queue.offer(new Card(1, "♠"));
3.2 PriorityQueue 的两种比较方式
方式一:通过 Comparable 实现
class Card implements Comparable<Card> {
@Override
public int compareTo(Card o) {
return this.rank - o.rank;
}
}
PriorityQueue<Card> queue = new PriorityQueue<>();
queue.offer(new Card(5, "♠"));
queue.offer(new Card(3, "♥"));
queue.offer(new Card(8, "♣"));
System.out.println(queue.poll().rank);
System.out.println(queue.poll().rank);
System.out.println(queue.poll().rank);
方式二:通过 Comparator 实现
class Card {
public int rank;
public String suit;
}
class CardComparator implements Comparator<Card> {
@Override
public int compare(Card o1, Card o2) {
return o2.rank - o1.rank;
}
}
PriorityQueue<Card> maxHeap = new PriorityQueue<>(new CardComparator());
maxHeap.offer(new Card(5, "♠"));
maxHeap.offer(new Card(3, "♥"));
maxHeap.offer(new Card(8, "♣"));
System.out.println(maxHeap.poll().rank);
System.out.println(maxHeap.poll().rank);
System.out.println(maxHeap.poll().rank);
3.3 PriorityQueue 内部实现原理
PriorityQueue 内部维护了一个比较器对象,根据用户是否提供比较器决定使用哪种比较方式:
public class PriorityQueue<E> {
private final Comparator<? super E> comparator;
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
this.comparator = comparator;
}
private void siftUp(int k, E x) {
if (comparator != null) {
siftUpUsingComparator(k, x);
} else {
siftUpComparable(k, x);
}
}
}
四、实战应用:使用 PriorityQueue 解决 Top-K 问题
Top-K 问题:从 N 个元素中找出最大(或最小)的 K 个元素。使用堆可以高效解决,时间复杂度为 O(NlogK)。
4.1 解决方案设计
解决思路(以找最小的 K 个数为例):
- 用前 K 个元素构建一个"大根堆"(堆顶是 K 个数中的最大值)
- 遍历剩余 N-K 个元素:
a. 如果当前元素 < 堆顶元素(即比 K 个数中的最大值还小)
b. 替换堆顶元素,并调整堆
- 遍历完成后,堆中的 K 个元素就是最小的 K 个数
4.2 完整代码实现
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
public class TopKProblem {
static class MinHeapComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
}
static class MaxHeapComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
}
public static int[] findSmallestK(int[] arr, int k) {
if (arr == null || k <= 0 || k > arr.length) {
return new int[0];
}
PriorityQueue<Integer> maxHeap = <>(k, ());
( ; i < k; i++) {
maxHeap.offer(arr[i]);
}
( k; i < arr.length; i++) {
arr[i];
maxHeap.peek();
(current < heapMax) {
maxHeap.poll();
maxHeap.offer(current);
}
}
[] result = [k];
( ; i < k; i++) {
result[i] = maxHeap.poll();
}
result;
}
[] findLargestK([] arr, k) {
(arr == || k <= || k > arr.length) {
[];
}
PriorityQueue<Integer> minHeap = <>(k, ());
( ; i < k; i++) {
minHeap.offer(arr[i]);
}
( k; i < arr.length; i++) {
arr[i];
minHeap.peek();
(current > heapMin) {
minHeap.poll();
minHeap.offer(current);
}
}
[] result = [k];
( ; i < k; i++) {
result[i] = minHeap.poll();
}
result;
}
{
[] array = {, , , , , , , , , };
;
[] smallestK = findSmallestK(array, k);
System.out.println( + k + + Arrays.toString(smallestK));
[] largestK = findLargestK(array, k);
System.out.println( + k + + Arrays.toString(largestK));
}
}
4.3 算法复杂度分析
- 空间复杂度:O(K)(只需要维护大小为 K 的堆)
- 时间复杂度:O(N log K)
建堆:O(K)
遍历剩余元素并调整:O((N-K) log K)
总复杂度:O(N log K)
4.4 使用 Lambda 表达式简化代码(Java 8+)
public static int[] findSmallestKLambda(int[] arr, int k) {
if (arr == null || k <= 0 || k > arr.length) {
return new int[0];
}
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(
k, (a, b) -> b - a
);
}
五、最佳实践与常见陷阱
5.1 比较器实现的注意事项
- 处理 null 值:明确约定 null 的排序位置
- 保持一致性:compareTo/compare 方法与 equals 方法保持一致
避免整型溢出:
return Integer.compare(o1.rank, o2.rank);
5.2 PriorityQueue 使用建议
- 线程安全:PriorityQueue 不是线程安全的,多线程环境使用 PriorityBlockingQueue
- 遍历顺序:PriorityQueue 的迭代器不保证按优先级顺序遍历
初始化容量:如果知道元素数量,建议指定初始容量
PriorityQueue<Integer> pq = new PriorityQueue<>(expectedSize);
5.3 综合示例:学生成绩排序系统
import java.util.*;
class Student {
private String name;
private int score;
private String className;
public Student(String name, int score, String className) {
this.name = name;
this.score = score;
this.className = className;
}
public static class ScoreComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return Integer.compare(s2.score, s1.score);
}
}
public static class ClassScoreComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
int classCompare = s1.className.compareTo(s2.className);
if (classCompare != 0) {
return classCompare;
}
return Integer.compare(s2.score, s1.score);
}
}
@Override
String {
String.format(, name, className, score);
}
{
List<Student> students = Arrays.asList(
(, , ),
(, , ),
(, , ),
(, , )
);
PriorityQueue<Student> scoreQueue = <>( ());
scoreQueue.addAll(students);
System.out.println();
(!scoreQueue.isEmpty()) {
System.out.println(scoreQueue.poll());
}
PriorityQueue<Student> classQueue = <>( ());
classQueue.addAll(students);
System.out.println();
(!classQueue.isEmpty()) {
System.out.println(classQueue.poll());
}
}
}
总结
Java 对象比较是面向对象编程中的重要概念,特别是在使用集合框架时。通过本文的学习,你应该掌握:
- 三种比较方式的核心区别:
equals:判断逻辑相等
Comparable:定义自然排序(内部比较器)
Comparator:定义多种排序规则(外部比较器)
- PriorityQueue 的比较机制:
- 优先使用构造器传入的 Comparator
- 否则依赖元素的 Comparable 实现
- 两者都未提供则抛出 ClassCastException