大家好呀!今天我们要聊一个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 的“暗坑”与解决方案(二)