10.Java对象比较完全指南:从equals到Comparable与Comparator 目录一、从一个报错开始为什么Card不能放进PriorityQueue1.1 问题复现1.2 Java中对象的比较方式概览二、基本类型的比较热身运动三、方式一覆写equals方法3.1 为什么需要覆写equals3.2 如何正确覆写equals3.3 equals的局限性四、方式二实现Comparable接口4.1 Comparable是什么4.2 如何实现Comparable4.3 使用Comparable4.4 Comparable的优缺点五、方式三使用Comparator比较器5.1 Comparator是什么5.2 如何实现Comparator5.3 使用Comparator5.4 匿名内部类和Lambda简化写法5.5 Comparator的优缺点六、三种方式对比总结七、实战用Comparator创建大小堆解决TopK问题7.1 思路分析7.2 代码实现7.3 时间复杂度分析八、总结与学习建议8.1 核心要点回顾8.2 如何选择8.3 学习建议写在前面本文基于课程《回顾Java对象的比较》内容整理结合个人学习笔记和实践经验编写。旨在帮助读者系统掌握Java中对象比较的三种方式。如需深入学习建议配合Oracle官方文档和JDK源码阅读。一、从一个报错开始为什么Card不能放进PriorityQueue1.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) { PriorityQueueCard 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. 覆写equalsequals()判断两个对象是否相等2. 实现ComparablecompareTo()自然排序类内部定义比较规则3. 实现Comparatorcompare()定制排序类外部定义比较规则这三种方式各有优劣下面逐一深入讲解。二、基本类型的比较热身运动在讲对象比较之前先回顾一下基本类型的比较这部分比较简单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 如何正确覆写equalspublic 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方法AltInsert → equals and hashCode比自己手写更可靠。3.3 equals的局限性equals只能判断相等或不相等无法回答谁大谁小的问题。而优先级队列需要的是大小关系大于、小于、等于所以equals在这里派不上用场。四、方式二实现Comparable接口4.1 Comparable是什么Comparable是java.lang包中的一个泛型接口定义如下public interface ComparableT { /** * 比较当前对象和参数对象 * return 负数当前对象小于参数对象 * 正数当前对象大于参数对象 * 零 两个对象相等 */ public int compareTo(T o); }4.2 如何实现Comparable让Card类实现Comparable接口并重写compareTo方法public class Card implements ComparableCard { 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 使用Comparablepublic 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了 PriorityQueueCard 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 ComparatorT { /** * 比较两个参数对象 * return 负数o1 o2 * 正数o1 o2 * 零 o1 o2 */ int compare(T o1, T o2); }注意区分Comparable是我比较我自己Comparator是第三方比较两个对象。5.2 如何实现Comparatorimport java.util.Comparator; // 定义一个比较器类按牌面值比较 class CardRankComparator implements ComparatorCard { 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 ComparatorCard { 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 使用Comparatorpublic 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 PriorityQueueCard 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表达式简化// 匿名内部类方式 PriorityQueueCard pq1 new PriorityQueue(new ComparatorCard() { Override public int compare(Card o1, Card o2) { return o1.rank - o2.rank; } }); // Lambda表达式方式更简洁 PriorityQueueCard pq2 new PriorityQueue( (o1, o2) - o1.rank - o2.rank ); // 方法引用方式最简洁 PriorityQueueCard pq3 new PriorityQueue( Comparator.comparingInt(c - c.rank) );5.5 Comparator的优缺点优点零侵入不需要修改Card类的源代码灵活多变可以为同一个类定义多种比较规则解耦比较逻辑和业务逻辑分离缺点需要额外定义比较器类代码量稍多六、三种方式对比总结对比维度equalsComparableComparator所属包java.lang.Objectjava.langjava.util方法名equals(Object)compareTo(T)compare(T, T)返回值booleanint负/零/正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 ComparatorInteger { Override public int compare(Integer o1, Integer o2) { return o2 - o1; // 降序大的优先 } } // 小根堆比较器 static class LessIntComp implements ComparatorInteger { 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(); PriorityQueueInteger 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]; } // 找最大用最小堆 PriorityQueueInteger 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要么传入ComparatorTopK问题找最小K个用大根堆找最大K个用小根堆8.2 如何选择场景推荐方式判断两个对象是否相等覆写equals类有天然的排序规则如按年龄排序学生实现Comparable需要多种排序规则定义多个Comparator无法修改类的源码使用ComparatorPriorityQueue需要自定义排序传入Comparator8.3 学习建议多写多练自己定义几个类分别用三种方式实现比较阅读源码看看Integer、String等类是如何实现Comparable的注意边界比较时注意null的处理、溢出问题o1 - o2可能溢出建议用Integer.compare(o1, o2)如果你觉得这篇文章对你有帮助欢迎点赞收藏。下一篇我们将继续探索Java集合框架的更多精彩内容敬请期待注本文为个人学习总结所有代码示例均为独立编写。建议读者在学习过程中结合JDK官方文档和源码进行验证。

本月热点