当前位置: 首页 > news >正文

面试官:你说说 HashMap 的 put 过程?我直接把这篇文章甩给他,当场通过!

前言

HashMap 作为 Java 集合框架中最常用的数据结构之一,几乎贯穿了我们日常开发的每个角落。同时它也是面试中的“常驻嘉宾”,从数据结构到线程安全,从 JDK 7 到 JDK 8 的变化,总能问出深度。

很多同学能背出“数组+链表+红黑树”,但当被问到扰动函数、扩容死循环或者为什么容量必须是 2 的幂时,就开始含糊。今天这篇文章,我们将从源码角度彻底拆解 HashMap 的 put 过程,把底层原理和面试考点一网打尽,建议收藏反复观看。


1. HashMap 的核心数据结构

在深入 put 操作之前,先快速过一下存储结构。

  • JDK 7:数组 + 单向链表(头插法)
  • JDK 8:数组 + 单向链表 + 红黑树(尾插法)

HashMap 内部维护了一个Node<K,V>[] table数组,每个数组元素称为“桶”(bucket)。当发生哈希冲突时,会以链表形式挂载到桶上。若链表长度≥ 8且数组长度≥ 64,链表树化为红黑树,从而将查询复杂度从 O(n) 降低到 O(log n);当红黑树节点数≤ 6时会退化为链表。

![基本结构](https://img-blog.csdnimg.cn/...图片占位,建议自行画图。)


2. put 操作的宏观流程

一句话总结:通过 key 的哈希值定位到桶,然后放入或更新值。浓缩成源码里的putVal方法:

public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }

关键步骤:

  1. 计算 key 的哈希值;
  2. 判断数组是否初始化,否则扩容/初始化;
  3. 通过哈希与数组长度取模定位桶;
  4. 如果桶为空,直接新建节点插入;
  5. 如果桶不为空,则在链表/红黑树中寻找 key,存在则更新,否则插入;
  6. 插入后判断链表是否需要树化;
  7. 最后判断是否需要扩容。

下面我逐步拆解每个细节,并附带简化的源码逻辑。


3. 哈希值计算 —— 扰动函数

HashMap 并没有直接使用key.hashCode(),而是经过了一次“扰动”:

static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
  • 为何扰动?
    取模时通常只用到了哈希值的低位,高位特征容易被浪费。扰动函数将高 16 位与低 16 位做异或,低位的随机性增强,从而减少碰撞。
  • 为何用异或而不是 & 或 |?
    异或能让 0 和 1 各保持 50% 的概率,分布最均匀,而位与偏向 0,位或偏向 1,都会加大碰撞。

这个细节面试极爱问,记住“让高位参与运算,减少冲突”。


4. 定位桶索引 —— 为何容量必须是 2 的幂?

putVal中,定位桶的代码是:

tab[i = (n - 1) & hash] // n 为数组长度

该语句等价于hash % n,但仅当 n 为 2 的幂时才成立。使用位运算比取模快很多,也是性能优化的关键。

容量始终为 2 的幂还有两个重要好处:

  • n-1二进制全为 1,哈希值的低位能完全参与索引运算;
  • 扩容时hash & oldCap可以快速判断节点在新数组中的位置(见后文扩容部分),无需重新计算哈希。

你一定见过的面试题:“为什么扩容总是 2 倍?” 答案就是以上这些。


5. 核心 put 过程(简化源码注释)

以下是putVal的精简解读,建议对照源码阅读:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 1. 数组若为空,先扩容/初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 2. 定位桶,若为空直接新建节点放入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 3. 桶内首个节点就是目标 key if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 4. 树结构下使用树的添加方法 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 5. 链表遍历查找/追加 else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 树化判断:链表长度 >= 8 - 1 且数组长度 >=64 才树化 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 6. 存在映射则覆盖 value if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 7. 判断是否需要扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

需要注意的点:

  • 树化条件binCount >= 7表示链表节点≥8,同时treeifyBin内部还会检查数组长度是否 < 64,若小于 64 会先扩容,而不是树化。
  • 遍历比较:先比较哈希值,再比较引用地址==,最后才用equals,这是性能优化。
  • threshold= capacity * loadFactor(默认 0.75),当 size 超过该阈值就扩容。

6. 扩容机制 —— 一个 2 倍的美妙数学

扩容方法resize()核心任务:

  • 新数组长度 = 旧数组长度 * 2
  • 重新分配旧节点到新数组

JDK 7 采用头插法并每个节点重新计算索引,导致多线程下可能产生环形链表,引发 CPU 100% 的死循环。
JDK 8 采用尾插法,且利用了一个巧妙的规律:扩容后节点要么在原位置,要么在原位置 + 旧容量的位置。

因为 2 次幂扩容,旧索引只依赖于哈希值的低位 bit;新索引增加了一位参与运算。用旧 hash & oldCap 进行判断:

  • (e.hash & oldCap) == 0,节点留在原索引;
  • 否则,节点移动到原索引 + oldCap。

源码中将同一位置的链表分化为两条高低链(loHead, hiHead),一次性移动,避免每次重建节点的开销,同时保证了顺序不变。

面试题 “HashMap 如何扩容?” 如果能答出高低链表优化和尾插法,面试官一定会对你刮目相看。


7. JDK 7 vs JDK 8 对比,一个表格搞定

对比项JDK 7JDK 8
数据结构数组 + 单向链表数组 + 链表 + 红黑树
插入方式头插法(导致resize死循环)尾插法
哈希值计算4次位运算+5次异或(更复杂)高16位异或低16位(更简洁)
扩容定位每个节点重新 hash 取模利用 hash & oldCap 分流,效率高
空元素存储允许 null 键值对(特殊处理)同左

这些区别是面试的高频考点,特别是死循环产生原因及如何避免:要么用 ConcurrentHashMap,要么牢记多线程下不要用 HashMap。


8. 常见面试题与解答思路

  1. HashMap 的 put 流程
    按照本文第 5 步中的 7 个步骤回答,突出扰动函数、链尾插入、树化、扩容触发。

  2. 为什么从头插法改成尾插法?
    头插法扩容时反转链表顺序,多线程 put 容易造成环,尾插法保持顺序不会成环。

  3. 为什么树化阈值是 8?
    根据泊松分布,在负载因子 0.75 下链表达到 8 的概率极低(千万分之一),是空间与时间的权衡。同时退化为 6 防止频繁转换。

  4. 负载因子为什么是 0.75 ?
    平衡了空间利用率和查找效率。过高会加大冲突,过低浪费空间。0.75 是官方反复测试的折中值。

  5. ConcurrentHashMap 与 HashMap 的区别?
    前者采用 CAS + synchronized 分段锁实现线程安全;HashMap 线程不安全,扩容时可能导致数据丢失或死循环。


9. 总结

本文从 put 操作这条主线出发,把 HashMap 的哈希扰动、取模优化、链表插入、树化策略、扩容机制以及 JDK 7/8 的变化全部串联了起来。看完这篇文章,你不仅能在面试中对 HashMap 的细节侃侃而谈,也能在写代码时避免很多性能坑和并发坑。

建议下一步:打开 IDE,在 HashMap 的putVal方法上打断点 Debug 一次,用笔记录下链表树化和扩容分链的完整过程,你的理解会再翻一倍。


如果这篇文章对你有帮助,点个赞、收藏支持一下,我会持续输出更多硬核 Java 技术干货。评论区说说你还想了解哪个集合的底层原理,我优先安排!

http://www.jsqmd.com/news/828318/

相关文章:

  • Cursor Free VIP终极指南:3步破解试用限制,永久免费使用AI编程助手
  • Windows ADB驱动一键安装:3分钟搞定Android设备连接难题
  • 想出国,需要考中式烹调师的看过来,简单考证 - 教育官方推荐官
  • ZhiLight:基于剪贴板的知乎内容净化工具设计与实现
  • 别再死记硬背辗转相除法了!用Python从‘更相减损术’到欧几里得,彻底搞懂GCD算法原理
  • 必要工作终结:普遍基本收入与 AI 驱动的繁荣
  • Homebrew SSL连接失败?除了换源和代理,你可能忘了检查这个Git仓库状态
  • 英飞凌BSC014N06NS渠道商
  • FanControl深度解析:Windows平台风扇智能控制完整实战指南
  • 全国热门的天康压力表代理商推荐:安徽国鹏环保科技有限公司 - 安互工业信息
  • 李彦宏的DAA,量得出智能体的繁荣,量不出用户的归属感
  • MCP协议实战:为AI智能体构建标准化地址查询工具
  • Cesium加载GeoJSON面数据,贴地后边界线消失?一个Polyline实体轻松搞定
  • 结构方程模型:R语言入门→SEM原理→lavaan全局估计→piecewiseSEM局域估计→blavaan/brms贝叶斯SEM
  • 智能纸张计数显示装置:基于电容传感技术的非接触式高精度检测方案
  • 2026西安市民真实黄金回收交易经历,对比七家门店最终选定闪闪珠宝全过程 - 西安闲转记
  • 学术期刊信息平台的技术架构简析——以某平台为例
  • 别再死记硬背了!用一张图搞懂ARM AMBA总线家族:APB、AHB、AXI到底怎么选?
  • TVA 在宠物混合监护场景中的创新应用(4)
  • 人社的中式烹调师怎么考,难不难,看这一篇就够了 - 教育官方推荐官
  • SystemVerilog中logic数据类型:统一reg与wire的设计实践
  • 怎样高效搭建AI多智能体交易系统:3步快速部署完整方案
  • 如何快速掌握明日方舟自动化助手:5大核心功能告别重复操作
  • 暗黑破坏神II角色编辑器:三步解锁终极游戏体验的完整指南
  • 1.2cubemx 配合 keil 点亮第一盏LED灯
  • 3分钟完成Windows系统优化:Chris Titus Tech WinUtil新手完全指南
  • 完整指南:如何使用UndertaleModTool轻松解包和修改Undertale游戏文件
  • 酒吧德州扑克娱乐小程序开发Java技术搭建源码案例
  • 科技中介机构如何提升服务能力与客户转化率?
  • Snap.Hutao胡桃工具箱:为什么这是原神玩家必备的终极桌面助手