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

通俗易懂解释Java8 HashMap

我们来用通俗易懂的方式解释一下 Java 8 中 HashMap 的原理,让你对它的结构、运行机制有清晰的理解。


🌳 什么是 HashMap?

HashMap 是 Java 中非常常用的数据结构,用于存储键值对(key-value)。你可以把它理解成一个“大数组”,每个位置可以存储一个或多个数据。


🧱 HashMap 的基本结构

Java 8 中的 HashMap 底层由 数组 + 链表 + 红黑树 组成:

HashMap└── 数组 table[]     ← 每个元素称为“桶”├── 链表/树    ← 存储实际的 key-value 节点(Node)

🛠️ 核心工作流程

我们来一步步看 HashMap 是怎么工作的:


✅ 插入 put(key, value)

举个例子:

map.put("apple", 100);

背后做了这些事:

  1. 计算 hash 值:

    • 对 key(“apple”)调用 hashCode(),然后做了一些扰动处理。
    • 比如 "apple".hashCode() = 93029210,经过扰动后变成一个新的 hash 值。
  2. 计算数组索引:

    • index = hash & (table.length - 1),定位到数组的某一桶。
  3. 判断该桶是否有元素:

    • 如果没有:直接放进去。

    • 如果有:判断 key 是否相同。

      • 相同:替换旧值。
      • 不同:链表/红黑树追加新节点。
  4. 链表转红黑树(Java8 新特性):

    • 当同一个桶内元素数量超过 8 个 且数组长度超过 64,会把链表转成红黑树,提高查询效率。

🔍 查询 get(key)

比如:

map.get("apple");
  1. 计算 hash。

  2. 定位到数组索引。

  3. 在对应的桶里找:

    • 是链表:顺序比较 key。
    • 是红黑树:用树的方式查找。

🔄 发生了哈希冲突怎么办?

多个不同的 key 计算后可能落到同一个数组位置(桶),这就叫 哈希冲突。Java 8 中:

  • 前几个冲突的 key 用 链表 挂在一起;
  • 如果冲突太多(链表长度 > 8 且数组长度 > 64),链表就会转成 红黑树,加快查找速度。

📈 扩容机制

HashMap 的默认容量是 16,装载因子是 0.75,当元素数量 > 16 * 0.75 = 12 时,就会发生 扩容

  1. 扩容为原来容量的 2 倍。
  2. 重新计算所有 key 的 hash 值和位置,迁移元素。

这也是为什么我们建议在使用 HashMap 时 预估容量大小,以避免多次扩容影响性能。


🧠 为什么要用红黑树?

因为链表查找是 O(n),而红黑树是 O(log n),当冲突严重时,查询效率会大幅下降。引入红黑树可以在极端情况下保持较好的性能。


🔥 小结图示:

插入:hash → 定位数组桶 → 空就放,有就比较 → 链表 / 红黑树
查询:hash → 定位 → 链表/树中查找 key
扩容:元素太多 → 数组翻倍 → 元素重新分布

✅ Java 8 中 HashMap 的优点

特性优点说明
引入红黑树避免链表过长导致查找变慢
扰动函数优化减少哈希冲突,分布更均匀
结构清晰数组 + 链表 + 红黑树,灵活扩展

我们来用通俗易懂的语言 + 举例方式,深入讲讲 Java 8 中 HashMap扰动函数(hash 扰动函数),帮助你真正理解它的作用和原理。


一、什么是扰动函数?

✅ 定义一句话:

扰动函数是为了让 hash 值的高位信息也能参与数组下标的计算,从而让 key 更均匀地分布在 HashMap 的数组中。


二、为什么需要扰动函数?

🚨 问题:低位碰撞严重!

在 HashMap 中,定位数组下标的方式是这样的:

index = hash(key) & (table.length - 1)

由于 table.length 是 2 的幂,这样的 & 操作会只保留 hash 值的低位

➡️ 如果两个 key 的 hash 值只有高位不同,而低位相同,它们就会落入同一个桶(发生冲突)。

🧠 举个例子:

hash1 = 0b10110000_00000000_00000000_00000001
hash2 = 0b01010000_00000000_00000000_00000001

虽然高位差很多,但最后的几位一样。由于我们只用低位参与数组下标计算,这两个 key 会落在同一个位置!

✅ 解决方案:扰动函数

通过扰动函数把 高位信息混合到低位,避免冲突更均匀。


三、Java 8 中的扰动函数源码分析

HashMap 中源码如下(简化):

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

🚀 解释:

  • h = key.hashCode():拿到 key 的原始 hashCode(32 位整数)。
  • h >>> 16:无符号右移 16 位,把高位移动到低位。
  • ^:做异或运算,把原来的高位和低位混合起来,让高位也参与到最终结果中

四、通俗举例讲解

假设我们有两个 key:

key1.hashCode() = 0b10110000_00000000_00000000_00000001
key2.hashCode() = 0b01010000_00000000_00000000_00000001

它们的低位是一样的,都会落到相同数组位置,发生 hash 冲突。

有了扰动函数:

扰动结果 = h ^ (h >>> 16)

扰动后:

  • key1 的高位会被移到低位并参与混合;
  • 即便低位一样,只要高位不同,扰动后结果就不一样。

这样就打乱了 hashCode 的原始规律,使得分布更随机,降低 hash 冲突概率。


五、总结(口诀记忆)

  • 🚩 低位计算索引,高位白白浪费。
  • 🧠 扰动函数混高低,分布均匀更合理。
  • 异或右移搞起来,冲突少,效率高。

六、动手试一试(简单代码示例)

public class HashDisturbExample {public static void main(String[] args) {String key = "apple";int h = key.hashCode();int disturbed = h ^ (h >>> 16);System.out.println("原始 hashCode: " + h);System.out.println("扰动后结果:     " + disturbed);}
}

运行后你会看到两个不同的值,这说明扰动函数确实改变了 hash 值结构,帮助分布更均匀。


✅ 第一个问题:为什么 table.length 是 2 的幂?

🔧 原因:

为了 优化取模(mod)操作为位运算(&)操作,提升性能!

🧠 解释:

HashMap 中,我们要根据 key 的 hash 值决定存放的位置,也就是数组下标:

index = hash(key) % table.length

如果 table.length 是任意数字,比如 10,那就必须使用 除法运算 %,这是一个比较慢的操作。

而如果 table.length 是 2 的幂,比如 16、32、64,我们就可以写成:

index = hash(key) & (table.length - 1)

⚠️ 这行代码的效果和 % 是一样的,但效率更高

🧠 举例说明:

假设 table.length = 16(即 2⁴),那么 16 的二进制是:

16 = 10000(即 2 的 4 次方)
16 - 1 = 15 = 01111(二进制)

这样做 & 15 相当于取 hash 值的 低 4 位,效果等同于 hash % 16,而且更快!


✅ 第二个问题:为什么 & 的操作和 % 是一样的?

🎯 只有在 table.length2 的幂时,才成立!

hash % 16 == hash & (16 - 1)

这是因为 2 的幂次减 1 得到的是全 1 的二进制位数。比如:

table.length二进制table.length - 1二进制
8100070111
16100001501111
3210000031011111

当你对一个数 x 执行 x & (n - 1),就等于 取 x 的低几位,这等价于 x % n(当 n 是 2 的幂时)。

✅ 举个例子:

hash = 0b101010        // 十进制 42
table.length = 16      // 2⁴
mask = 16 - 1 = 15 = 0b111142 % 16 = 10
42 & 15 = 0b101010 & 0b1111 = 0b1010 = 10

结果一样,但是 & 运算效率远高于 % 运算。


✅ 第三个问题:为什么是“低位”参与运算?

📌 因为 hash & (table.length - 1) 只保留 低位信息

你刚刚也看到了,举个例子:

hash = 0b10110000_00000000_00000000_00001101
table.length = 16 (2),mask = 0b00000000_00000000_00000000_00001111

执行 &

hash & mask =
0b10110000_00000000_00000000_00001101
&
0b00000000_00000000_00000000_00001111
=
0b00000000_00000000_00000000_00001101 => 13

➡️ 所以只有 低 4 位被保留下来,高位被舍弃了!


🤔 那高位的 hash 值不是白计算了吗?

是的,如果不做处理,高位信息就浪费了,这就是为啥我们需要:

🚀 扰动函数:h ^ (h >>> 16)

它会把高 16 位的信息也混入低 16 位中,确保高位不会被浪费。


✅ 总结归纳:

问题解答
为什么 table.length 是 2 的幂?为了将取模 % 优化成更快的 & 位运算。
为什么 & 可以代替 %当除数是 2 的幂时,x & (n - 1) 等价于 x % n,而效率更高。
为什么是低位参与?因为 & 操作只保留了低位的信息(如 & 15 保留低 4 位),高位信息被忽略。

🧠 为什么要扩容?

HashMap 是通过数组 + 链表(或红黑树)来存储键值对的。它的性能依赖于:

键值对在数组中的分布是否均匀(冲突少)

如果 HashMap 存放的元素太多,冲突越来越多,链表变长,性能会急剧下降,所以需要:

扩容(resize):将原数组变大,把原来的元素“重新分布”到新的数组中。


⚙️ HashMap 扩容时机

HashMap 会在满足以下条件时扩容:

当前元素个数 > 阈值(threshold) = 容量 × 负载因子(loadFactor)
  • 默认初始容量是 16
  • 默认负载因子是 0.75
  • 所以默认第一次扩容是在元素数量超过 16 × 0.75 = 12 时发生

🚀 扩容做了什么?

当发生扩容时,HashMap 会做几件事:

  1. 将数组容量扩大为 原来的两倍
  2. 创建一个新的数组(新的 table)
  3. 重新计算每个键值对的索引(index)
  4. 将所有旧元素 “搬家” 到新数组中

📊 举个例子来说明

假设你有一个容量为 4 的 HashMap,插入了 3 个元素(已接近 0.75 的阈值):

原数组(长度4):
index 0: [A -> 1]
index 1: [B -> 2]
index 2: [C -> 3]
index 3: null

你再插入一个元素 D -> 4,此时元素数目为 4,超过 4 × 0.75 = 3,触发扩容。

扩容过程:

  • 数组变为长度 8
  • 所有键的 hash 会重新计算 index
  • 每个键值对会根据新的 index 放到新数组中,分布更均匀
扩容后数组(长度8):
index 0: null
index 1: [A -> 1]
index 2: null
index 3: [C -> 3]
index 4: null
index 5: [B -> 2]
index 6: [D -> 4]
index 7: null

这样就避免了原来在小数组中“拥挤”的问题。


🔁 元素“搬家”的优化:位运算重分布

Java 8 做了一个优化,在扩容时 不再重新计算 hash 值,而是通过如下逻辑判断元素是留在原位置还是移动到“新位置”:

// 假设 oldCap 是原数组长度,newCap = oldCap * 2
if ((e.hash & oldCap) == 0) {// 留在原地
} else {// 移动到原位置 + oldCap
}

这个技巧利用了 2 的幂次 的特性,通过 hash & oldCap 直接判断是否需要挪动,提高了效率!


💡 为什么扩容不是一开始就做得很大?

为了节省内存。HashMap 的容量和负载因子设计是为了在性能和内存之间做权衡。


✅ 总结:HashMap 扩容机制

步骤内容
扩容时机元素个数 > 阈值(容量 × 负载因子)
扩容操作数组变为原来的两倍
元素处理每个元素根据新数组重新定位(高效位运算)
优化手段hash & oldCap 判断是否搬家
目的降低 hash 冲突,提高查询性能
http://www.lryc.cn/news/608529.html

相关文章:

  • 计数组合学7.11(RSK算法)
  • 人工智能与农业:智慧农业的发展与未来
  • 数据集-目标检测系列- 地球仪 数据集 globe>> DataBall
  • SmartCLIP:具有识别保证的模块化视觉-语言对齐
  • 代码随想录刷题Day23
  • linux 启动流程?
  • 拉格朗日插值法
  • 数据库理论
  • 深入 Go 底层原理(七):逃逸分析
  • 商品中台数据库设计
  • Flutter dart运算符
  • 【Leetcode】2561. 重排水果
  • 嵌入式第十八课!!数据结构篇入门及单向链表
  • 数据结构(12)二叉树
  • 计算学习理论(PAC学习、有限假设空间、VC维、Rademacher复杂度、稳定性)
  • Java内存模型(Java Memory Model,JMM)
  • 网安-中间件-weblogic(updating..)
  • 数据结构初学习、单向链表
  • 暑期算法训练.13
  • 什么是DOM和BOM?
  • 智能手表:电源检查
  • 入门MicroPython+ESP32:安装逗脑IDE及驱动
  • JVM 03 类加载机制
  • 堆----1.数组中的第K个最大元素
  • 高效游戏状态管理:使用双模式位运算与数学运算
  • 关于人工智能AI>ML>DL>transformer及NLP的关系
  • springboot大学生成绩管理系统设计与实现
  • NCV8402ASTT1G自保护N沟道功率MOSFET安森美/ONSEMI 过流过温保护汽车级驱动NCV8402ASTT1
  • 动态规划经典模型:双数组问题的通用解决框架与实战
  • Vue3核心语法进阶(computed与监听)