Java HashMap未调用equals()时匹配正确Key的原理问题
首先明确测试场景的前置条件:
- 自定义
Person类的hashCode()固定返回1,因此所有Person实例作为HashMap键时,都会被分配到哈希表的同一个桶中,形成单链表(测试仅存入3个元素,未达到红黑树转换阈值8) - 自定义
equals()固定返回false,且方法被调用时会打印日志 - 测试用例中put和get操作传入的是同一个
Person对象引用,不是内容相同的不同实例
1. 为什么equals()始终返回false时,HashMap仍能定位到正确的键?
HashMap匹配键的逻辑从来不是只靠equals()方法,JDK中HashMap的核心匹配逻辑(putVal、getNode方法共用该判断)如下:
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
判断是严格的短路逻辑,执行顺序为:
- 先比较两个键的哈希值是否相等,不相等直接跳过当前节点
- 再比较两个键是否是同一个对象引用(用
==判断),如果是直接判定匹配,不会执行后续逻辑 - 只有引用不相等时,才会调用
equals()方法做内容层面的相等判断
测试中get传入的person3就是之前put时存入的同一个对象,第二步==判断直接命中,根本不会走到永远返回false的equals()方法,自然能定位到正确的值。
2. 为什么get操作时没有调用equals()就匹配到了键值对?
这完全是上述短路判断逻辑导致的:
当遍历到桶中存储person3的节点时,首先哈希值匹配,接着==判断发现节点的key和传入的person3是同一个引用,逻辑或||直接返回true,后面的equals()调用被完全短路,因此控制台看不到equals方法的打印。
你贴的初始测试用例里person3是第一个存入的元素,是对应桶链表的头节点,get时第一个遍历到的就是它,所以全程不会触发任何equals调用,和给出的运行输出完全吻合。
3. 为什么插入顺序会影响get时equals的调用次数?HashMap不是不维护插入顺序吗?
这里要纠正一个常见认知偏差:
HashMap「不维护插入顺序」指的是全局遍历顺序不保证和插入顺序一致——元素会根据哈希值分散到不同的桶中,不同桶之间的遍历顺序由桶在哈希表的位置决定,和插入顺序无关。但单个哈希桶内部的单链表,节点排列顺序完全和插入顺序强相关:JDK8的HashMap采用尾插法,新节点在判定无重复键后会追加到链表尾部,没有扩容重排的情况下链表节点顺序就是严格的插入顺序。
测试中所有键都落在同一个桶的单链表里,get操作遍历链表的规则是从链表头(第一个插入的节点)开始逐个向后比对:
- 如果
person3是第一个插入的节点:它就是链表头,第一个比对就通过==命中,不会调用任何equals - 如果
person3是第二个插入的节点:链表顺序是「第一个插入的节点 → person3节点」,先比对第一个节点:哈希匹配但==不成立,触发1次equals调用(返回false),再比对person3节点命中,总共调用1次equals - 如果
person3是第三个插入的节点:链表顺序是「第一个插入的节点 → 第二个插入的节点 → person3节点」,比对前两个节点时各触发1次equals调用,总共2次,比对到person3时==命中,不会触发person3的equals调用
这就是观察到的插入顺序和equals调用次数关联的底层原因,和HashMap不保证全局插入顺序的结论并不冲突。
补充说明
当前重写的equals()和hashCode()完全违反JavaSE规范:equals()固定返回false意味着任何两个对象都不相等,hashCode()固定返回1会导致所有键哈希冲突,这是仅用于原理验证的极端反例,生产环境禁止这么写,否则会导致HashMap性能退化为O(n)链表,且仅能通过完全相同的对象引用取值,内容相同的不同实例无法正常匹配键。
内容的提问来源于stack exchange,提问作者Klaus Köhler

