10.Java对象比较完全指南:从equals到Comparable与Comparator

目录

一、从一个报错开始:为什么Card不能放进PriorityQueue?

[1.1 问题复现](#1.1 问题复现)

[1.2 Java中对象的比较方式概览](#1.2 Java中对象的比较方式概览)

二、基本类型的比较:热身运动

三、方式一:覆写equals方法

[3.1 为什么需要覆写equals?](#3.1 为什么需要覆写equals?)

[3.2 如何正确覆写equals?](#3.2 如何正确覆写equals?)

[3.3 equals的局限性](#3.3 equals的局限性)

四、方式二:实现Comparable接口

[4.1 Comparable是什么?](#4.1 Comparable是什么?)

[4.2 如何实现Comparable?](#4.2 如何实现Comparable?)

[4.3 使用Comparable](#4.3 使用Comparable)

[4.4 Comparable的优缺点](#4.4 Comparable的优缺点)

五、方式三:使用Comparator比较器

[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的优缺点)

六、三种方式对比总结

七、实战:用Comparator创建大小堆解决TopK问题

[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的标准套路

  1. 自反性:和自己比,肯定相等

  2. 非空性:和null比,肯定不等

  3. 类型检查:必须是同类才能比较

  4. 内容比较:按照业务逻辑判断是否相等

小提示:实际开发中,IDEA可以自动生成equals方法(Alt+Insert → equals and hashCode),比自己手写更可靠。

3.3 equals的局限性

equals只能判断"相等"或"不相等",无法回答"谁大谁小"的问题。而优先级队列需要的是大小关系(大于、小于、等于),所以equals在这里派不上用场。


四、方式二:实现Comparable接口

4.1 Comparable是什么?

Comparablejava.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是什么?

Comparatorjava.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个数,需要一个大根堆:

  1. 用前K个元素建大根堆

  2. 遍历剩余元素,如果比堆顶小,替换堆顶并调整

  3. 遍历结束后,堆中的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 核心要点回顾

  1. 基本类型 直接用 ><==比较

  2. 对象比较有三种方式:

    • equals:判断相等,不能比大小

    • Comparable:自然排序,侵入性强,一种规则

    • Comparator:定制排序,零侵入,多种规则

  3. PriorityQueue要求元素可比,要么实现Comparable,要么传入Comparator

  4. 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官方文档和源码进行验证。

相关推荐
LiaoWL1236 小时前
【JUC合集-01】JUC并发编程复习总结
java
快乐星空Maker6 小时前
C++【生存游戏】开发:荒岛往事 第三期
开发语言·c++·游戏·编程语言
郝学胜-神的一滴6 小时前
Qt 高级编程 035:无边框窗口阴影以及圆角双特效实现
开发语言·c++·qt·程序人生·用户界面
正在走向自律6 小时前
实战心得:利用PaddleOCR彻底解决大模型无法解析图片型PDF的问题
开发语言·pdf·视觉检测·paddleocr·视觉模型·离线ocr识别
怒放de生命20106 小时前
【web3基础】go-zero创建rpc服务,创建配置文件(二)
开发语言·rpc·golang·web3
麻瓜老宋7 小时前
AI开发C语言应用按步走,我的基础开发环境
c语言·开发语言
见叶之秋7 小时前
C细节补充
c语言·开发语言
Tisfy7 小时前
LeetCode 1979.找出数组的最大公约数:模拟(附手动gcd)
java·数学·算法·leetcode·题解·最大公约数
她说彩礼65万7 小时前
C# yeild的使用
开发语言·c#