news 2026/7/5 6:28:37

01 面试官:HashMap 的扩容为什么是 2 的幂?很多人背了答案,但没一个说到点子上

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
01 面试官:HashMap 的扩容为什么是 2 的幂?很多人背了答案,但没一个说到点子上


先讲个真事。

几年前我面一个哥们,简历上写着“精通 Java 集合框架”。我问了他这道题,他张嘴就来:“为了减少哈希冲突,扩容时不需要重新计算索引位置。”

对,标准答案一个字不差。

然后我问了他第二句——“那 HashMap 的容量为什么不能设成 100?100 不是 2 的幂,但也能用啊。”

他愣住了。沉默了三秒之后说了句大实话:“没想过。”

那次面试他没挂,但我对他的评价从“扎实”降到了“背过”。


标准答案是什么

这道题随便找个面经都能找到标准答案,大概是这么几句:

HashMap 的容量用 2 的幂,是因为(n - 1) & hash等价于hash % n,位运算比取模运算快。扩容的时候也不需要对每个元素重新算哈希值,元素要么留在原位,要么移到「原位 + 旧容量」的位置。

这些都是对的。但如果你只答到这一层——面试官不会说你错,但他会接着往下挖。

因为他在乎的根本不是你知不知道结论,而是你有没有真的搞懂为什么


面试官真正在考你什么

我当面试官的时候,这道题其实在测三件事。

第一件:位运算和取模到底什么关系?

这是一个很多同学卡住的地方。我尽量讲得简单点。

先看一个生活例子:

假设有个大转盘,转盘上有 16 个格子,编号 0 到 15。你闭着眼睛扔飞镖,飞镖落在几号格子?你根本不知道,因为镖可能落在范围外。怎么让它永远落在 0-15 之间?很简单,你拿编号对 16 取余数:

编号 20 → 20 ÷ 16 = 1 余 4 → 落在 4 号格子 编号 50 → 50 ÷ 16 = 3 余 2 → 落在 2 号格子 编号 16 → 16 ÷ 16 = 1 余 0 → 落在 0 号格子

这个“除以 16 取余数”就是取模,HashMap 本质上要干的就是这件事——把任意一个 key 的 hash 值,映射到数组的某个位置。

那为什么 HashMap 不用%而用&

因为计算机做除法(取模本质是除法)比做位运算慢了不是一个数量级。

但位运算有个限制——&操作符不能直接替代%,除非你的除数是 2 的幂。

我举个例子你就懂了。

假设数组长度是 16,16 的二进制是1 0000。16 - 1 = 15,15 的二进制是1111

现在有一个 hash 值,比如 47,它的二进制是10 1111。拿 47 和 15 做按位与:

47 → 10 1111 (32 + 8 + 4 + 2 + 1 = 47) 15 → 00 1111 (8 + 4 + 2 + 1 = 15) ————————————————————————————————————————— & → 00 1111 (8 + 4 + 2 + 1 = 15)

得到 15。你再用 47 除以 16 取余数:47 ÷ 16 = 2 余 15。结果一样!

为什么会一样?因为 15 的二进制全是一串 1,跟它做按位与,就相当于只保留了 hash 值的低 4 位。而保留低 4 位,恰好就等于对 16 取模。

如果长度不是 2 的幂会怎样?比如长度是 10:

10 - 1 = 9, 9 的二进制是 1001 47 → 10 1111 9 → 00 1001 ————————————————————————————————————————— & → 00 1001 (9)

但 47 ÷ 10 = 4 余 7。结果对不上了!因为 9 的二进制不是全 1,低位有 0,按位与的时候把它“吃掉”了,造成了不均匀。

所以结论很简单:容量必须是 2 的幂,是因为只有 2 的幂减 1 的二进制全是 1,才能用&代替%


第二件:扩容迁移为什么不需要重算?

面试官听到你说“扩容时不需要重新算哈希位置”——他真正想确认的是:你理不理解为什么不需要重算

扩容的时候,数组长度从 16 变成 32。原来用 hash 的低 4 位决定位置,现在用低 5 位。

关键来了——低 4 位跟原来一样,多出来的只是第 5 位。如果第 5 位是 0,位置不变;如果第 5 位是 1,位置变成原位置 + oldCap

JDK 源码里是这么写的。直接看resize()方法中的关键部分:

// JDK 1.8 HashMap.resize() 扩容迁移逻辑 Node<K,V> loHead = null, loTail = null; // 低位链表(位置不变) Node<K,V> hiHead = null, hiTail = null; // 高位链表(位置 + oldCap) Node<K,V> next; do { next = e.next; // 关键判断:e.hash & oldCap // 如果结果是 0,说明新增的那一位是 0,位置不变 // 如果结果不是 0,说明新增的那一位是 1,位置要变 if ((e.hash & oldCap) == 0) { // 保持原位,加入低位链表 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { // 移到新位置(原位置 + 旧容量),加入高位链表 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 低位链表放回原位 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 高位链表放到 j + oldCap 的位置 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; }

这段代码是整个扩容优化的精华。它没有对每个元素重新计算 hash 位置,它只做了一件事——e.hash & oldCap看那一个新增的 bit 是 0 还是 1

举个例子:

原来长度是 16(0001 0000) hash 值假设是 23(0001 0111) 扩容前:hash & (16-1) = 23 & 15 = 0001 0111 & 0000 1111 = 0111 = 位置 7 扩容后长度是 32(0010 0000) 扩容后新位置 = 23 & (32-1) = 23 & 31 = 0001 0111 & 0001 1111 = 1 0111 = 位置 23 看:23 = 7 + 16 = 原位置 7 + 旧容量 16 为什么?因为 hash 的第 5 位(从 0 开始数)是 1,所以加上 oldCap。

e.hash & oldCap就是专门用来检查“新增的那一位是 0 还是 1”的:

e.hash = 23 → 0001 0111 oldCap = 16 → 0001 0000 ————————————————————————————————————————— & 0001 0000 ≠ 0 → 第 5 位是 1,要迁移到高位

JDK 1.7 是怎么干的?1.7 没有这个优化,它老老实实对每个元素重新hash % newCapacity。这也是为什么 JDK 1.8 的扩容效率高很多。


第三件:JDK 1.7 扩容为什么会死循环?

这个属于加分项。如果你在回答了 1.8 的优化后,能主动提到 1.7 的死循环问题,面试官会非常舒服。

JDK 1.7 扩容时的transfer()方法用的是头插法——新元素插到链表头部。源码长这样:

// JDK 1.7 HashMap.transfer() —— 头插法,会形成环形链表! void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while (null != e) { Entry<K,V> next = e.next; // ① 先记下下一个节点 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; // ② 把 e 的 next 指向新数组当前位置的头 newTable[i] = e; // ③ 把 e 放到新数组当前位置的头部 e = next; // ④ 继续处理下一个 } } }

为什么头插法会死循环?假设两个线程同时触发扩容,线程 A 在执行到第 ① 句后被挂起,线程 B 完成了全部扩容操作。等线程 A 恢复执行时,链表的方向已经被翻转了,A 拿着旧的 next 指针继续跑,最终 A→B 和 B→A 互相指向对方,形成一个环形链表

下次有人调用get()的时候,遍历链表永远走不到头,CPU 直接飙到 100%。

JDK 1.8 改成了尾插法,不会反转链表顺序,死循环问题也就不存在了。


tableSizeFor:一个被严重低估的骚操作

还有一个很多人不知道的点——当你new HashMap(10)的时候,实际容量不是 10,是 16。

因为 HashMap 的构造器里调了一个叫tableSizeFor()的方法,它会自动把你传的数向上取到第一个大于等于它的 2 的幂。源码就这么几行:

// JDK 1.8 HashMap.tableSizeFor() // 作用:返回第一个大于等于 cap 的 2 的幂 // 你传 10 → 返回 16 // 你传 20 → 返回 32 static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }

这个算法非常优雅。它的思路是:先减 1,然后通过不断右移再或运算,把最高位 1 后面的所有位都变成 1,最后加 1 就得到了 2 的幂。

举个例子,cap = 10 → 二进制 1010:

第一步:n = cap - 1 = 9 → 1001 第二步:n |= n >>> 1 → 1001 | 0100 = 1101 第三步:n |= n >>> 2 → 1101 | 0011 = 1111 第四步:n |= n >>> 4 → 1111 | 0000 = 1111 ... 后面几步不变 最后:n + 1 = 1111 + 1 = 10000 = 16

面试的时候如果你能提一句“对了,tableSizeFor 的那个移位算法还挺巧妙的”——面试官就知道你翻过源码,不是背的。


8 年老兵说句实话

这道题我问了几百次,我把面试者分三档,你可以对号入座:

第一档:只答“减少哈希冲突”——及格线。说明看过面经,但没钻进去。

第二档:能答出“位运算代替取模快”+“扩容时位置不用重算”——不错了。大部分面试官会翻篇问下一题。

第三档:在第二档的基础上,随口加一句“因为 (n-1) 的二进制全 1,按位与就是保留 hash 低位,等价于取模”。只多了 20 个字,面试官直接在心里把你的评级从“背过面经”调成“基础扎实”。

如果再能提到 JDK 1.7 的头插法死循环和 tableSizeFor 的移位算法——恭喜,这道题的满分就已经拿到了。

我这么多年面试最大的感受就是:面试官和你无冤无仇,不会故意刁难你。他只是在找一个信号,一个能让他确定“这个人真的懂,不是背的”的信号。

你多说的那一句话,往往就是那个信号。


你面试的时候被问到过这道题吗?你是怎么答的?来留言区唠唠


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

Ubuntu安装PostgreSQL

PostgreSQL 默认支持所有 Ubuntu 版本。然而&#xff0c;Ubuntu “快照”特定版本的PostgreSQL&#xff0c;随后在整个范围内得到支持 该Ubuntu版本的生命周期。 PostgreSQL 项目维护一个支持 Apt 的仓库 PostgreSQL的可用性。 https://www.postgresql.org/download/linux/ubun…

作者头像 李华
网站建设 2026/7/5 6:24:45

从团购内卷到 AI 搜索:生成式引擎优化 (GEO) 底层技术拆解与本地实体落地选型指南

摘要 本地生活实体门店传统流量渠道出现明显增长瓶颈:线下地推转化效率持续走低,第三方团购平台佣金成本挤压盈利空间。随着 LLM 大模型成为用户本地消费检索主流入口,GEO(Generative Engine Optimization,生成式引擎优化) 成为适配 AI 检索逻辑的新一代数字营销技术。本…

作者头像 李华
网站建设 2026/7/5 6:24:36

PixelMap 转化为 URI:HarmonyOS NEXT 完整指南

一、为什么 PixelMap 不能直接转 URI&#xff1f;在 HarmonyOS NEXT 中&#xff0c;这两个类型有本质区别&#xff1a;类型本质存储位置用途PixelMap内存中的像素位图数据内存&#xff08;RAM&#xff09;图片编辑、显示、处理URI文件路径标识字符串&#xff08;如 file://...&…

作者头像 李华
网站建设 2026/7/5 6:22:11

3步解锁网易云音乐隐藏功能:BetterNCM安装器让你告别单调播放器

3步解锁网易云音乐隐藏功能&#xff1a;BetterNCM安装器让你告别单调播放器 【免费下载链接】BetterNCM-Installer 一键安装 Better 系软件 项目地址: https://gitcode.com/gh_mirrors/be/BetterNCM-Installer 你是否厌倦了千篇一律的网易云音乐界面&#xff1f;是否希望…

作者头像 李华
网站建设 2026/7/5 6:21:17

Linux应急响应实战:从入侵检测到溯源加固的完整流程解析

1. 项目概述&#xff1a;应急响应靶机“WhereIS”的定位与价值最近在安全圈子里&#xff0c;应急响应能力的实战演练越来越受重视。光看理论、背流程&#xff0c;真遇到攻击事件时还是会手忙脚乱。于是&#xff0c;各种模拟真实攻击现场的“靶机”应运而生&#xff0c;它们就像…

作者头像 李华
网站建设 2026/7/5 6:20:51

如何3分钟为网易云音乐安装插件管理器:BetterNCM安装工具完整指南

如何3分钟为网易云音乐安装插件管理器&#xff1a;BetterNCM安装工具完整指南 【免费下载链接】BetterNCM-Installer 一键安装 Better 系软件 项目地址: https://gitcode.com/gh_mirrors/be/BetterNCM-Installer 你是否厌倦了网易云音乐一成不变的界面和有限的功能&…

作者头像 李华