大家好呀!今天我们要聊一个Java中超级重要但又经常让人迷糊的话题——对象比较和Hash相关的正确写法!😊 作为一个Java开发者,如果你搞不清楚equals()、hashCode()这些方法,就像厨师分不清盐和糖一样可怕!💥
这篇文章会从最基础的概念讲起,一直深入到HashMap的实现原理,保证让你彻底搞明白!文章有点长,但绝对值得收藏慢慢看哦~📚
一、先来认识Java中的"=="和equals() 👋
1.1 "=="操作符:我只看表面!
在Java中,"=="是最简单的比较方式,但它有个特点:对于基本类型比较值,对于引用类型比较内存地址。
int a = 5;
int b = 5;
System.out.println(a == b); // true ✅ 值相同
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false ❌ 不同对象
🤔 为什么第二个是false呢?因为s1和s2虽然内容相同,但它们是两个不同的对象,住在内存的不同地方!
1.2 equals()方法:我想看内涵!
equals()是Object类的方法,默认实现其实就是"==":
public boolean equals(Object obj) {
return (this == obj);
}
但很多类(如String、Integer)会重写这个方法,让它比较内容而不是地址:
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1.equals(s2)); // true ✅ 内容相同
👨🍳 小贴士:对于自己定义的类,如果要比较内容,必须重写equals()方法!
二、如何正确重写equals()方法 ✍️
2.1 equals()方法的五个黄金法则
重写equals()不是随便写的,必须遵守以下规则:
自反性:x.equals(x) 必须为true对称性:x.equals(y) 和 y.equals(x) 结果必须相同传递性:如果x.equals(y)且y.equals(z),那么x.equals(z)必须为true一致性:多次调用equals()结果应该一致(除非对象被修改)非空性:x.equals(null) 必须为false
2.2 手把手教你重写equals()
假设我们有一个Person类:
public class Person {
private String name;
private int age;
// 构造方法、getter/setter省略...
}
正确重写equals()的步骤:
检查是否同一个对象引用(性能优化)检查是否为null检查是否是同一个类类型转换比较关键字段
完整实现:
@Override
public boolean equals(Object o) {
// 1. 检查是否是同一个对象
if (this == o) return true;
// 2. 检查是否为null或类不匹配
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换
Person person = (Person) o;
// 4. 比较关键字段
return age == person.age &&
Objects.equals(name, person.name);
}
🔍 注意:使用Objects.equals()来比较字段可以避免NullPointerException!
三、hashCode()方法:equals()的好基友 🤝
3.1 为什么需要hashCode()?
hashCode()返回对象的哈希码,主要用于哈希表(如HashMap、HashSet)中快速定位对象。
黄金规则:如果两个对象equals()为true,它们的hashCode()必须相同!反之则不一定。
3.2 hashCode()的三个约定
一致性:同一对象多次调用hashCode()应返回相同值(除非对象被修改)相等性:如果equals()为true,hashCode()必须相同不等性:如果equals()为false,hashCode()最好不同(但不是必须)
3.3 如何正确重写hashCode()
继续用Person类举例,我们可以使用Java 7引入的Objects.hash()方法:
@Override
public int hashCode() {
return Objects.hash(name, age);
}
👩💻 内部实现:实际上是把各个字段的hashCode组合起来计算
3.4 为什么equals()和hashCode()要一起重写?
想象HashMap的工作原理:
先计算key的hashCode()确定存储位置如果该位置有元素,再用equals()比较是否真的相同
如果只重写equals()不重写hashCode():
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // false ❌
Map map = new HashMap<>();
map.put(p1, "Alice的数据");
System.out.println(map.get(p2)); // null 😱 因为hashCode不同找不到!
💡 结论:equals()和hashCode()必须保持逻辑一致!
四、深入理解Hash相关集合 🏗️
4.1 HashMap的工作原理
HashMap是Java中最常用的哈希表实现,它的性能关键就在于hashCode()!
存储过程:
计算key的hashCode()通过哈希函数确定数组下标如果该位置为空,直接存入如果不为空,用equals()比较key
相同:覆盖value不同:形成链表(Java 8后可能转为红黑树)
查找过程:
计算key的hashCode()定位到数组位置用equals()比较链表/树中的元素
4.2 好的hashCode()应该什么样?
均匀分布:不同对象尽量有不同的hashCode高效计算:不能太复杂稳定性:对象不变时hashCode不变
4.3 为什么String适合做HashMap的key?
因为String的hashCode()实现非常好:
public int hashCode() {
int h = hash; // 缓存hash值
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
特点:
使用31作为乘数(素数,分布均匀)计算结果会被缓存基于字符内容计算
五、高级话题:可变对象与Hash 🎯
5.1 为什么可变对象不适合做HashMap的key?
Person p = new Person("Alice", 25);
Map map = new HashMap<>();
map.put(p, "Alice的数据");
p.setName("Bob"); // 修改了key!
System.out.println(map.get(p)); // 可能返回null 😱
原因:
存储时计算的hashCode基于"Alice"修改后hashCode变成了基于"Bob"get()时去错误的位置查找,找不到!
5.2 解决方案
使用不可变对象作为key(如String、Integer)如果必须用可变对象,确保修改后不再作为key使用或者重写hashCode()使其不依赖可变字段
六、性能优化技巧 ⚡
6.1 缓存hashCode
对于不可变对象,可以缓存hashCode:
private int hash; // 默认为0
@Override
public int hashCode() {
if (hash == 0) {
hash = Objects.hash(name, age);
}
return hash;
}
6.2 选择合适的哈希字段
不是所有字段都需要参与hashCode计算:
@Override
public int hashCode() {
return Objects.hash(id); // 只用唯一标识字段
}
6.3 避免哈希冲突
哈希冲突会影响HashMap性能,可以通过:
增加哈希表的初始容量调整负载因子(默认0.75)设计更好的hashCode()方法
七、常见陷阱与错误 🕳️
7.1 陷阱1:忘记重写hashCode()
// 只重写了equals()忘记hashCode()
@Override
public boolean equals(Object o) { ... }
// 使用HashSet时会出问题
Set set = new HashSet<>();
set.add(new Person("Alice", 25));
System.out.println(set.contains(new Person("Alice", 25))); // 可能返回false
7.2 陷阱2:使用可变字段计算hashCode
@Override
public int hashCode() {
return Objects.hash(name, age, someList); // someList是可变的!
}
7.3 陷阱3:equals()实现不严谨
// 错误的equals()实现 - 缺少类型检查
@Override
public boolean equals(Object o) {
Person p = (Person)o; // 直接转换可能ClassCastException
return this.age == p.age;
}
八、最佳实践总结 🏆
总是同时重写equals()和hashCode():就像一对双胞胎,不能分开使用Objects工具类:Objects.equals()和Objects.hash()让代码更安全简洁优先选择不可变对象作为HashMap的key测试你的equals()和hashCode():确保遵守所有约定考虑使用IDE生成:大多数IDE可以生成可靠的equals()和hashCode()
九、实战演练 🛠️
让我们通过一个完整例子巩固所学:
public class Employee {
private final int id; // 唯一标识,不可变
private String name;
private Department department;
// 构造方法等省略...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee)) return false;
Employee employee = (Employee) o;
return id == employee.id; // 只比较id
}
@Override
public int hashCode() {
return id; // 只用id计算hashCode
}
// getter/setter...
}
enum Department {
IT, HR, FINANCE, MARKETING
}
这个实现:
使用不可变的id作为唯一标识equals()和hashCode()只基于id即使name或department变化,也不会影响作为key的使用
十、Java 14新特性:record类 📌
Java 14引入了record类,它自动实现了equals()、hashCode()和toString():
public record Person(String name, int age) {}
使用record作为key非常安全,因为:
所有字段都是final的(不可变)自动生成的equals()和hashCode()基于所有字段简洁明了,减少样板代码
结语 🌈
恭喜你坚持看到了这里!🎉 现在你应该已经掌握了:
✅ Java对象比较的两种方式:"=="和equals() ✅ 如何正确重写equals()和hashCode() ✅ HashMap等哈希集合的工作原理 ✅ 各种最佳实践和常见陷阱
记住,正确的对象比较和哈希实现是编写健壮Java程序的基础!下次面试被问到HashMap原理时,你就可以自信满满地解释了!💪
如果有任何问题,欢迎在评论区留言讨论哦~😊 我们下次再见!
推荐阅读文章
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
HTTP、HTTPS、Cookie 和 Session 之间的关系
什么是 Cookie?简单介绍与使用方法
什么是 Session?如何应用?
使用 Spring 框架构建 MVC 应用程序:初学者教程
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
如何理解应用 Java 多线程与并发编程?
把握Java泛型的艺术:协变、逆变与不可变性一网打尽
Java Spring 中常用的 @PostConstruct 注解使用总结
如何理解线程安全这个概念?
理解 Java 桥接方法
Spring 整合嵌入式 Tomcat 容器
Tomcat 如何加载 SpringMVC 组件
“在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”
“避免序列化灾难:掌握实现 Serializable 的真相!(二)”
如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)
解密 Redis:如何通过 IO 多路复用征服高并发挑战!
线程 vs 虚拟线程:深入理解及区别
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
“打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”
Java 中消除 If-else 技巧总结
线程池的核心参数配置(仅供参考)
【人工智能】聊聊Transformer,深度学习的一股清流(13)
Java 枚举的几个常用技巧,你可以试着用用
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
HTTP、HTTPS、Cookie 和 Session 之间的关系
使用 Spring 框架构建 MVC 应用程序:初学者教程
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
Java Spring 中常用的 @PostConstruct 注解使用总结
线程 vs 虚拟线程:深入理解及区别
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)
为什么用了 @Builder 反而报错?深入理解 Lombok 的“暗坑”与解决方案(二)