news 2026/5/15 12:23:53

彻底拆解 Java HashMap 扩容机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
彻底拆解 Java HashMap 扩容机制

彻底拆解 Java HashMap 扩容机制

Java HashMap 是最常用的集合类之一,其扩容(resize)机制是其高效性和动态性的核心。扩容发生在元素数量超过阈值时,目的是减少哈希冲突、保持 O(1) 的平均访问时间。下面从基础原理到源码细节,进行彻底拆解(基于 OpenJDK 17–21 版本,机制基本稳定;JDK 8+ 引入红黑树优化)。

我会结合源码分析、流程图、表格和示例,确保清晰易懂。如果你想看具体版本源码差异,可以提供 JDK 版本,我再细化。

1. HashMap 基础回顾(为什么需要扩容?)

HashMap 底层是数组 + 链表/红黑树的结构:

  • 数组(table):存储桶(bucket),默认大小 16(2^4),总是 2 的幂次方(便于位运算优化)。
  • 负载因子(load factor):默认 0.75,表示数组“满”到多少比例时扩容。平衡内存和性能(太高冲突多,太低浪费空间)。
  • 阈值(threshold):容量 * 负载因子。当 size > threshold 时,触发扩容。
  • 哈希冲突:通过链表(或树)解决,但链表过长会退化为 O(n),扩容能均匀分布元素。

扩容的核心目标:翻倍容量,重新分布元素,减少冲突。

2. 扩容触发条件

扩容不是随意发生的,主要在put()操作中检查:

  • 条件size >= threshold且 当前桶不为空(优化:如果桶为空,添加元素不会立即扩容)。
  • 时机:通常在putVal()方法中,添加元素后检查。
  • 默认值
    • 初始容量:16
    • 负载因子:0.75
    • 初始阈值:12(16 * 0.75)

示例:添加第 13 个元素时,size=13 > 12,触发扩容。

源码片段(从 HashMap.putVal()):

finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){// ... 省略哈希计算和桶定位if(++size>threshold)// size 自增后检查resize();// 触发扩容// ...}
3. 扩容过程详解(resize() 方法)

扩容的核心是resize()方法:创建新数组,迁移旧元素。

步骤分解

  1. 计算新容量

    • 新容量 = 旧容量 * 2(oldCap << 1)。
    • 如果旧容量已达最大(2^30),阈值设为 Integer.MAX_VALUE。
    • 更新阈值 = 新容量 * 负载因子。
  2. 创建新数组

    • Node<K,V>[] newTab = new Node[newCap];
  3. 迁移元素(rehash)

    • 遍历旧数组的所有桶。
    • 对于每个桶的链表/树:
      • 如果是单个节点,直接计算新位置:newIndex = (hash & (newCap - 1))
      • 如果是链表:使用位运算优化拆分链表(无需全量重新哈希)。
      • 如果是树:拆分树为两个子链表(如果子链表 > 6,转回链表)。
  4. 优化点:由于容量是 2 的幂,rehash 只需检查 hash 的高位 bit(oldCap 对应的 bit)。

    • 如果(e.hash & oldCap) == 0:留在原位置(lo 链)。
    • 否则:移到oldIndex + oldCap(hi 链)。

流程伪代码(简化版):

Node<K,V>[]resize(){intoldCap=table.length;// 旧容量intnewCap=oldCap<<1;// 新容量 = 旧 * 2intnewThr=newCap*loadFactor;// 新阈值Node<K,V>[]newTab=newNode[newCap];// 新数组for(intj=0;j<oldCap;++j){// 遍历旧桶Node<K,V>e=oldTab[j];if(e!=null){oldTab[j]=null;// 清空旧桶if(e.next==null){// 单节点newTab[e.hash&(newCap-1)]=e;}elseif(einstanceofTreeNode){// 树节点((TreeNode<K,V>)e).split(this,newTab,j,oldCap);// 拆树}else{// 链表Node<K,V>loHead=null,loTail=null;// 低位链Node<K,V>hiHead=null,hiTail=null;// 高位链do{Node<K,V>next=e.next;if((e.hash&oldCap)==0){// 关键优化:检查高位 bitif(loTail==null)loHead=e;elseloTail.next=e;loTail=e;}else{if(hiTail==null)hiHead=e;elsehiTail.next=e;hiTail=e;}e=next;}while(e!=null);// 放置链if(loTail!=null){loTail.next=null;newTab[j]=loHead;}if(hiTail!=null){hiTail.next=null;newTab[j+oldCap]=hiHead;}}}}table=newTab;// 更新 tablethreshold=newThr;returnnewTab;}

可视化流程(假设容量从 16 → 32):

  • 旧桶 index = hash & 15(4 bits)
  • 新桶 index = hash & 31(5 bits)
  • 第 5 bit (16=2^4) 决定:0 → 原位;1 → 原位 + 16

例如:

  • hash = 0b00001(1) → 新位 = 1 & 31 = 1(原位)
  • hash = 0b10001(17) → 新位 = 17 & 31 = 17(原位 + 16)
4. 扩容中的关键优化与特性
特性 / 优化说明为什么重要?
容量总是 2^n哈希计算用& (cap-1)代替% cap,位运算更快。性能提升,避免慢的模运算。
位运算拆分链表无需为每个节点重新计算完整 hash,只检查一个 bit。扩容时间从 O(n) 降到 O(链表长度)。
树化 / 去树化JDK8+:链表 >8 时转红黑树;扩容后若子链表 <=6,转回链表。防最坏 O(n) 退化,提高极端场景性能。
延迟扩容仅当 size > threshold 且桶不空时扩容。避免不必要的扩容,节省资源。
最大容量2^30(1<<30),超过后不再扩容(阈值 = Integer.MAX_VALUE)。防止内存溢出。
5. 扩容的性能影响与注意事项
  • 时间复杂度:扩容 O(n),因为需遍历所有元素。但平均分摊到每次 put() 是 O(1)。
  • 内存开销:扩容时临时双倍内存(旧+新数组),GC 后释放旧数组。
  • 并发问题
    • JDK7 问题:多线程扩容可能导致链表环(无限循环)。源码中链表头插法逆序,导致线程竞争时循环。
    • JDK8+ 修复:使用尾插法 + 位运算,避免环。但 HashMap 仍非线程安全,推荐 ConcurrentHashMap。
  • 自定义负载因子:构造函数可设(e.g., new HashMap(16, 0.5f)),低负载因子减少冲突,但浪费内存。
  • 初始容量建议:预估元素数,设为 (预计 size / 0.75) 的下一个 2^n,避免多次扩容。

示例:扩容前后变化

HashMap<String,Integer>map=newHashMap<>();// 容量16,阈值12for(inti=0;i<13;i++){map.put("key"+i,i);// 第13个时扩容到32}System.out.println(map.size());// 13// 内部:table 从16→32,所有元素重新分布
6. 常见问题解答
  • 扩容后哈希不变吗?不变,但 index 可能变(多一位 bit 参与计算)。
  • 为什么翻倍而不是加固定值?翻倍保持 2^n,优化位运算;加固定值需全量重哈希,慢。
  • 空 HashMap 何时扩容?添加第一个元素时,初始化 table=16,阈值=12。
  • HashMap vs Hashtable?Hashtable 线程安全,但扩容机制类似(翻倍+1,奇数容量)。
  • 生产优化:用new HashMap((int)(expectedSize / 0.75f) + 1)预设容量,避免扩容。
总结一句话

Java HashMap 扩容机制通过阈值触发 + 容量翻倍 + 位运算优化迁移,实现了高效动态增长,同时在 JDK8+ 结合树化防退化,确保了高负载下的性能稳定性。

想深入某个部分?如 ConcurrentHashMap 扩容对比、性能测试代码、或特定 JDK 版本差异?随时告诉我~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/11 5:46:18

从S锁/X锁到Next-Key Lock:MySQL锁机制硬核拆解

从 S 锁 / X 锁 到 Next-Key Lock&#xff1a;MySQL InnoDB 锁机制硬核拆解 MySQL 的 InnoDB 引擎锁机制是面试和生产中高频考点&#xff0c;尤其是幻读如何被解决、Next-Key Lock 到底锁了什么、加锁规则如何判断等。下面从基础到进阶&#xff0c;一层层拆解。 1. 锁的分类总…

作者头像 李华
网站建设 2026/5/9 0:27:39

PPML 估计 + 一般均衡求解?ge_gravity2 一套 Stata 命令全搞定

温馨提示&#xff1a;若页面不能正常显示数学公式和代码&#xff0c;请阅读原文获得更好的阅读体验。 丁闪闪 (lianxhcn163.com) 曾咏新 厦门大学 (zengyongxinhpe163.com) 提要&#xff1a;本文系统整理了金融大语言模型 (LLM) 研究的核心资源&#xff0c;包括 12 个主流金融数…

作者头像 李华
网站建设 2026/5/12 1:22:25

leetcode 930. Binary Subarrays With Sum 和相同的二元子数组

Problem: 930. Binary Subarrays With Sum 和相同的二元子数组 前缀和&#xff0c;哈希表记录每个和所在的索引i&#xff0c;对goal0分开讨论的&#xff0c;使用前缀和- goal&#xff0c;拿到s prefixSum[i1] - goal;&#xff0c;数可能的子数组个数&#xff0c;并累加 Code …

作者头像 李华
网站建设 2026/5/12 1:23:05

探秘AI教材写作!这些工具能让你的教材生成过程低查重率

在编写教材的过程中&#xff0c;总是能精准触及“慢节奏”带来的种种问题。尽管已经准备好了框架和资料&#xff0c;却总是在内容写作上卡住——一段话反复推敲半个小时&#xff0c;还是觉得表达不够准确&#xff1b;章节间的过渡连接&#xff0c;更是绞尽脑汁也想不出合适的词…

作者头像 李华
网站建设 2026/5/12 1:23:06

四光吊舱多光谱融合技术解析

四光吊舱的多光谱融合&#xff0c;核心在于将可见光、热成像等不同波段的传感器数据进行协同处理和智能分析。这不仅能让你“看见”&#xff0c;更能让你“看透”复杂场景。多光谱融合模块的技术要点这项技术主要围绕硬件集成、算法处理和环境适应三个层面展开&#xff0c;下表…

作者头像 李华
网站建设 2026/5/12 1:22:53

AI写教材就选它!专业工具打造低查重优质教材,提升效率!

许多教材编写者常常感到遗憾&#xff1a;尽管他们精心撰写了教材的正文&#xff0c;但却因为缺少配套资源&#xff0c;导致整体的教学效果大打折扣。课后练习的题型设计需要有层次感&#xff0c;然而常常缺乏新颖的创意&#xff1b;想要制作直观的教学课件&#xff0c;却又没有…

作者头像 李华