文章目录
- 前言
- 一、先搞懂:hashCode 到底有什么用?
- 1. 哈希码的本质:一个 “身份标识” 的简化版
- 2. 核心应用场景:哈希集合的高效操作
- 3. 关于 hashCode 的两个重要约定
- 二、再理清:equals 方法的本职工作
- 1. Object 类的默认实现
- 2. 重写 equals 的正确姿势
- 三、关键核心:为什么重写 equals 必须重写 hashCode?
- 1. 反例:只重写 equals,不重写 hashCode
- 2. 反例分析:违背了 hashCode 的等价性约定
- 3. 正确做法:重写 equals 时,必须重写 hashCode
- 四、总结:hashCode 与 equals 的核心关联
- 五、面试高频坑点提醒
前言
大家好,我是程序员梁白开,今天我们聊一聊hashCode 与 equals。
在 Java 面试中,hashCode和equals绝对是高频考点,很多同学都能说出 “重写 equals 必须重写 hashCode”,但问到两者到底有什么用、为什么要强制绑定,却常常含糊其辞。今天就带大家从底层原理到实际应用,彻底搞懂这两个方法的 “爱恨情仇”。
一、先搞懂:hashCode 到底有什么用?
hashCode() 是 Java 中 Object 类的一个原生方法,它的核心作用是返回对象的哈希码值(int 类型),这个哈希码主要用于快速查找,是 HashMap、HashSet 等哈希集合的 “性能基石”。
1. 哈希码的本质:一个 “身份标识” 的简化版
你可以把哈希码理解为对象的一个 “简化指纹”。理论上,每个对象都有自己的内存地址(独一无二),但直接用内存地址作为标识进行查找,效率并不高。
hashCode() 会通过特定算法,将对象的内存地址或内部属性转化为一个 int 整数,这个整数就是哈希码,它的核心价值在于缩小查找范围。
2. 核心应用场景:哈希集合的高效操作
我们以HashMap为例,看看 hashCode 是如何发挥作用的:
- 当向 HashMap 中 put 元素 时,会先计算 key 的 hashCode,根据 hashCode 直接定位到对应的哈希桶(数组下标)。
- 如果该哈希桶为空,直接将键值对存入;如果不为空,再通过 equals() 方法比较桶内元素与新 key 是否相等:
- 相等则覆盖旧值;
- 不相等则以链表或红黑树的形式挂载(解决哈希冲突)。
- 当从 HashMap 中 get 元素 时,同样先计算 key 的 hashCode,快速定位到哈希桶,再通过 equals() 精准匹配目标元素。
试想一下,如果没有 hashCode,每次查找都要遍历 HashMap 中的所有元素,时间复杂度会从 O (1) 退化到 O (n),在数据量大的场景下,性能差距会极其明显。
3. 关于 hashCode 的两个重要约定
根据 Java 官方文档,hashCode() 需要遵循以下通用约定,这是我们重写方法的准则:
- 一致性:在同一个 Java 程序执行期间,对同一个对象多次调用 hashCode(),必须返回相同的整数,前提是对象用于 equals() 比较的属性没有被修改。
- 等价性:如果两个对象通过 equals() 方法比较为相等,那么它们的 hashCode() 必须返回相同的整数。
- 非唯一性:如果两个对象通过 equals() 方法比较为不相等,它们的 hashCode() 可以相同(这就是哈希冲突),但建议不同,以提高哈希集合的性能。
二、再理清:equals 方法的本职工作
equals() 同样是 Object 类的原生方法,它的核心作用是 判断两个对象是否 “逻辑相等”。
1. Object 类的默认实现
Object 类中 equals() 的源码如下:
publicbooleanequals(Objectobj){return(this==obj);}可以看到,默认的 equals() 本质上是 比较两个对象的内存地址,也就是判断两个引用是否指向同一个对象。
但在实际开发中,我们往往需要的是 “逻辑相等”。比如,对于一个 User 类,只要 id 相同,我们就认为两个 User 对象是相等的,这时候就需要 重写 equals() 方法。
2. 重写 equals 的正确姿势
以 User 类为例,重写 equals() 的规范写法:
publicclassUser{privateLongid;privateStringname;// 构造方法、getter/setter 省略@Overridepublicbooleanequals(Objecto){// 1. 自反性:自己和自己比较,返回 trueif(this==o)returntrue;// 2. 非空性 + 类型判断:避免空指针,且确保是同一类if(o==null||getClass()!=o.getClass())returnfalse;// 3. 类型强转,比较核心属性Useruser=(User)o;returnObjects.equals(id,user.id);// 用 Objects.equals 避免空指针}}重写 equals() 时,要遵循 自反性、对称性、传递性、一致性 这四个原则,这里不再展开,感兴趣的同学可以查阅官方文档。
三、关键核心:为什么重写 equals 必须重写 hashCode?
这是面试的核心问题,我们用 反例 来理解这个强制要求的必要性。
1. 反例:只重写 equals,不重写 hashCode
假设我们只重写了 User 类的 equals() 方法(按 id 比较),但没有重写 hashCode(),此时 Object 类的默认 hashCode() 会根据对象内存地址生成哈希码。
publicstaticvoidmain(String[]args){Useru1=newUser(1L,"张三");Useru2=newUser(1L,"李四");// 因为 id 相同,equals 返回 trueSystem.out.println(u1.equals(u2));// true// 但默认 hashCode 基于内存地址,u1 和 u2 是不同对象,哈希码不同System.out.println(u1.hashCode());// 比如:123456System.out.println(u2.hashCode());// 比如:789012// 放入 HashSet 中HashSet<User>set=newHashSet<>();set.add(u1);set.add(u2);// 预期:因为 u1 和 u2 相等,set 中应该只有一个元素// 实际:set 中存在两个元素!System.out.println(set.size());// 输出 2}2. 反例分析:违背了 hashCode 的等价性约定
上面的代码中,u1 和 u2 通过 equals() 比较为相等,但它们的 hashCode() 却不相同,这就 违背了 hashCode 的第二个约定。
当把这两个对象放入 HashSet 时:
- 存入 u1:计算 u1 的 hashCode,定位到哈希桶 A,存入。
- 存入 u2:计算 u2 的 hashCode,定位到哈希桶 B,存入。
- 由于两个对象在不同的哈希桶中,HashSet 不会再调用 equals() 进行比较,最终导致两个 “相等” 的对象被同时存入集合,破坏了 HashSet 的 “元素唯一性” 特性。
3. 正确做法:重写 equals 时,必须重写 hashCode
我们为 User 类补充 hashCode() 的重写,保证相等的对象具有相同的哈希码:
@OverridepublicinthashCode(){// 基于 equals 中比较的核心属性 id 生成哈希码returnObjects.hash(id);}此时再运行上面的测试代码:
- u1.equals(u2) 为 true,u1.hashCode() 和 u2.hashCode() 也相同。
- 存入 HashSet 时,u2 会定位到和 u1 相同的哈希桶,通过 equals() 比较后发现相等,不会被重复存入。
- 最终 set.size() 输出 1,符合预期。
四、总结:hashCode 与 equals 的核心关联
| 维度 | hashCode | equals |
|---|---|---|
| 核心作用 | 生成对象哈希码,用于快速查找 | 判断两个对象逻辑相等 |
| 调用时机 | 哈希集合(HashMap/HashSet)添加、查询元素时优先调用 | 哈希集合中定位到同一哈希桶后,用于精准匹配 |
| 关联规则 | 相等的对象,hashCode 必须相同 | 相同 hashCode 的对象,equals 不一定相等 |
一句话总结:
hashCode 是 “粗筛”,帮我们快速缩小查找范围;equals 是 “细筛”,帮我们精准判断对象是否相等。两者协同工作,才能保证哈希集合的高效与正确性。
五、面试高频坑点提醒
- 不要用随机数生成 hashCode:违反一致性约定,同一对象多次调用 hashCode 会返回不同值。
- 不要只重写 hashCode 而不重写 equals:没有意义,哈希集合依然无法正确判断元素唯一性。
- 重写 hashCode 时,要基于 equals 中的核心属性:比如 equals 比较 id 和 name,hashCode 也要包含这两个属性。