HashMap 的“江湖”生存指南:散列表、拉链与树化の绝技

39 阅读4分钟

摘要:你以为 HashMap 只是个简单的键值对“储物柜”?不,它是一个有自己江湖规矩的小世界!本文将带你潜入 HashMap 的底层,看它如何用“哈希值”定乾坤,用“拉链法”解纷争,又在关键时刻“树化”变身,平息江湖动乱。一文读懂,面试不慌!


开场白:一个高效的“储物柜”

想象一下,你有一个超级大的储物柜阵列,有无数个小格子。你想存一个键值对 ("手机", iPhone15)。笨办法是从第一个格子开始找,看哪个空着放哪,但这样取的时候也得从头找,太慢了!

HashMap 是个聪明人,它不干这种傻事。它的秘诀在于:“看钥匙,直接定位”

第一式:哈希函数 —— “定乾坤”

当你要存 ("手机", iPhone15) 时,HashMap 会先对钥匙 "手机" 做一件事:调用 hashCode() 方法

这个操作就像是给这把钥匙施加了一个“魔法”,把它变成了一个整数(哈希值)。这个整数,理论上就是这个物品独一无二的“身份证号”。

但是,储物柜的格子(数组)是有限的,比如默认只有16个(初始容量)。怎么把这个巨大的身份证号映射到有限的格子里呢?HashMap 会用这个哈希值和一个神秘数字进行 &(与运算),最终得到一个数组下标。

这就好比:最终位置 = hashCode("手机") & (数组长度 - 1)

这个公式能保证算出来的位置,肯定在数组范围之内。完美!直接定位,一步到位。

第二式:拉链法 —— “化干戈为玉帛”

理想很丰满,现实很骨感。万一两把不同的钥匙(比如 "手机""话费"),经过魔法变换和计算后,指向了同一个格子怎么办?这就是著名的 “哈希冲突”

难道把先来的扔出去?不不不,HashMap 的解决方案尽显智慧——“拉链法”

每个格子,它不直接放物品,而是挂着一个链表(在 Java 8 之前)。当发生冲突时,新的键值对会以 Node 节点的形式,被接到这个链表的后面。

!()[example.com/lalianfa.pn…] (此处应有一张拉链法的示意图)

所以,当你根据 "话费" 找到这个格子时,会发现这里挂着一串节点。它会顺着链表往下找,一边找一边对比原始的钥匙 key(使用 equals() 方法),直到找到真正的 "话费" 为止。

所以,HashMap 的底层结构是:数组 + 链表。数组负责高速定位,链表负责解决冲突。

第三式:树化变身 —— “忍无可忍,无需再忍”

链表虽好,但如果一个格子下面挂的节点太多了,比如成百上千个,查询起来又得遍历,效率就退化成 O(n) 了,这简直是“江湖动乱”的前兆。

Java 8 之后,HashMap 引入了一个大招:当链表长度超过一定阈值(默认是8),并且当前数组容量达到一定程度时,这个链表会进行“树化”,也就是转身一变,成为一个红黑树

红黑树是一种自平衡的二叉搜索树,它能让在这个节点上的查找时间复杂度从 O(n) 降为 O(log n)

这好比: 一个村的村民(节点)一开始只是排成一队(链表),人少还好管理。后来人太多了,找个人得从头问到尾。于是村长决定,把队伍改造成一个有组织的家族树(红黑树),按照辈分、年龄排列,找人的时候通过家谱快速定位,效率大大提升!

当然,当树上的节点因为删除而变少(小于6)时,它还会退化成链表,毕竟维护一棵树也是需要成本的。

番外篇:扩容 —— “江湖扩容,海阔天空”

随着你存的东西越来越多,格子(数组)快不够用了,链表和树也会越来越长,冲突概率增加,效率下降。此时,HashMap 会启动 “扩容”

它会创建一个新的、更大的数组(通常是原来的2倍),然后把旧数组里所有的节点 “重新计算位置”,迁移到新数组上。这个过程叫做 rehash

虽然扩容本身比较耗时,但之后由于数组变大了,冲突会显著减少,整个 Map 的查询和插入效率又会得到提升。这就像一个小镇发展成了大城市,道路更宽,交通更顺畅。

总结与面试金句

  • 底层结构:数组 + 链表 / 红黑树。
  • 核心思想:通过哈希算法将键映射到数组下标,实现快速访问。
  • 解决冲突:拉链法。
  • 性能优化:链表过长时树化为红黑树;元素过多时进行扩容。

记住,equals() 方法判断相等,则 hashCode() 必须返回相同的值,这是你在 HashMap 江湖里安身立命的根本法则!

希望这篇“江湖指南”能让你对 HashMap 的理解更上一层楼!如果觉得有趣又有用,不妨点个赞吧~