HashMap 的底层结构详解:原理、put和get示例

embedded/2025/3/5 11:57:58/

HashMap 的底层结构详解

在 Java 中,HashMap 是基于哈希表实现的键值对存储结构,其核心设计目标是高效的数据存取(理想情况下时间复杂度为 O(1))。以下是其底层结构的详细解析:


1. 基本结构:数组 + 链表/红黑树

HashMap 的底层是一个 Node<K,V>[] table 数组,每个数组元素称为 桶(Bucket)。每个桶中存储以下两种数据结构之一:

  • 链表:默认结构,用于解决哈希冲突(同一哈希值的键值对按链表顺序存储)。
  • 红黑树(Java 8+):当链表长度超过阈值(默认为 8)时,链表转换为红黑树,以提高查询效率。
java">// HashMap 的节点定义(链表节点)
static class Node<K,V> implements Map.Entry<K,V> {final int hash;    // 哈希值final K key;V value;Node<K,V> next;    // 指向下一个节点的指针
}

2. 哈希函数与索引计算

HashMap 通过哈希函数将键(Key)映射到数组索引,具体步骤如下:

  1. 计算键的哈希值
    java">int hash = key.hashCode();  // 调用键的 hashCode() 方法
    
  2. 扰动函数优化
    Java 8 通过高 16 位异或低 16 位,减少哈希冲突。
    java">hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    
  3. 计算桶索引
    java">index = (table.length - 1) & hash;  // 等价于 hash % table.length(当 length 是 2 的幂时)
    

示例

  • 假设 table.length = 16(二进制 10000),则 table.length - 1 = 15(二进制 1111)。
  • 哈希值 hash = 356(二进制 101100100),则 index = 356 & 15 = 4

3. 哈希冲突处理

当不同键的哈希值映射到同一索引时,称为 哈希冲突HashMap 采用 链地址法 解决冲突:

  • 链表模式:新节点插入链表尾部(Java 8 前为头部)。
  • 红黑树模式(Java 8+):当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树;当节点数 ≤ 6 时,红黑树退化为链表。

链表转红黑树的阈值

  • 链表长度 ≥ TREEIFY_THRESHOLD(默认 8)。
  • 数组长度 ≥ MIN_TREEIFY_CAPACITY(默认 64)。

hashing_58">4. 扩容机制(Rehashing)

HashMap 的容量和负载因子(Load Factor)决定了扩容行为:

  • 初始容量:默认 16DEFAULT_INITIAL_CAPACITY)。
  • 负载因子:默认 0.75DEFAULT_LOAD_FACTOR)。
  • 扩容阈值阈值 = 当前容量 × 负载因子。当元素数量超过阈值时,触发扩容。

扩容流程

  1. 新容量为旧容量的 2 倍(保证容量始终为 2 的幂)。
  2. 重新计算所有键的哈希值和索引,分配到新数组中。
  3. 链表或红黑树节点按新索引重新分布。

示例

  • 初始容量 16,负载因子 0.75,阈值 12。当插入第 13 个元素时,触发扩容至 32。

5. 红黑树的优势
  • 时间复杂度优化:链表的查询时间复杂度为 O(n),而红黑树为 O(log n)。
  • 平衡性:红黑树通过颜色标记和旋转操作,保持近似平衡,插入/删除/查找操作高效。
  • 适用场景:哈希冲突严重时(如大量键的哈希值相同),红黑树显著提升性能。

6. 线程安全性

HashMap 非线程安全,多线程并发修改可能导致数据不一致或死循环(Java 7 链表头插法问题)。

  • 替代方案
    • ConcurrentHashMap:分段锁或 CAS 机制实现线程安全。
    • Collections.synchronizedMap():包装为同步集合。

7. 核心参数与默认值

参数默认值说明
DEFAULT_INITIAL_CAPACITY16初始容量(必须是 2 的幂)
DEFAULT_LOAD_FACTOR0.75负载因子(空间与时间的权衡)
TREEIFY_THRESHOLD8链表转红黑树的阈值
UNTREEIFY_THRESHOLD6红黑树退化为链表的阈值
MIN_TREEIFY_CAPACITY64链表转红黑树的最小数组长度

8. 性能优化建议

  1. 合理初始化容量:避免频繁扩容。
    java">// 预计存储 1000 个元素,负载因子 0.75,初始容量设为 2048(2^11)
    Map<String, Object> map = new HashMap<>(2048);
    
  2. 优化键的 hashCode():减少哈希冲突。
  3. 避免在多线程环境中直接使用 HashMap:选择 ConcurrentHashMap

9. 插入示例(put)

我们通过一个具体的示例逐步分析 HashMap 的底层结构变化,包括 数组、链表、红黑树 的形态。假设初始容量为 8,负载因子 0.75,阈值 6(8 * 0.75 = 6),当元素数量超过 6 时触发扩容。以下是详细步骤:

示例代码

java">HashMap<String, Integer> map = new HashMap<>(8);  // 初始容量 8
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
map.put("d", 4);
map.put("e", 5);
map.put("f", 6);
map.put("g", 7);     // 触发扩容
map.put("h", 8);
map.put("i", 9);
map.put("j", 10);
map.put("k", 11);    // 触发链表转红黑树

步骤 1:初始化数组(容量 8)

java">Node<K,V>[] table = new Node[8];

数组初始状态(索引 0~7 均为空):

索引: 0   1   2   3   4   5   6   7↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓null null null null null null null null

步骤 2:插入前 6 个元素(不触发扩容)

2.1 插入 a=1
  • 计算 a 的哈希值:假设 hash("a") = 97
  • 计算索引:index = (8-1) & 97 = 1(97 二进制是 11000017111,按位与后为 1)。
  • 插入到索引 1:
索引: 0   1       2   3   4   5   6   7↓   ↓       ↓   ↓   ↓   ↓   ↓   ↓null [a=1]  null null null null null null
2.2 插入 b=2
  • 假设 hash("b") = 98,索引 98 & 7 = 2
索引: 0   1       2       3   4   5   6   7↓   ↓       ↓       ↓   ↓   ↓   ↓   ↓null [a=1]  [b=2]   null null null null null
2.3 插入 c=3f=6
  • 假设哈希值均匀分布到不同索引:
索引: 0   1       2       3   4   5   6   7↓   ↓       ↓       ↓   ↓   ↓   ↓   ↓null [a=1]  [b=2]  [c=3][d=4][e=5][f=6] null

此时元素数量 6,达到阈值 6,但尚未触发扩容(Java 中实际在插入第 7 个元素时扩容)。


步骤 3:插入 g=7(触发扩容)

3.1 扩容前
  • 插入 g=7,假设 hash("g") = 103,索引 103 & 7 = 7
索引: 0   1       2       3   4   5   6   7↓   ↓       ↓       ↓   ↓   ↓   ↓   ↓null [a=1]  [b=2]  [c=3][d=4][e=5][f=6][g=7]
  • 元素数量 7 > 6,触发扩容。
3.2 扩容后
  • 新容量为 16(旧容量 8 * 2)。
  • 重新计算所有元素的索引(index = hash & 15)。

假设哈希值重新计算后的分布:

索引: 0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓null [a=1][b=2] ... [d=4] ... [f=6][g=7] ... ... ... ... ... ... 

(具体分布取决于哈希值,这里假设均匀分布)


步骤 4:插入 h=8k=11(触发链表转红黑树)

4.1 插入 h=8j=10

假设 h=8i=9j=10 的哈希值均映射到索引 1:

索引 1:
↓
[a=1] → [h=8] → [i=9] → [j=10]  // 链表长度 4
4.2 插入 k=11
  • 假设 k=11 的哈希值也映射到索引 1,链表长度变为 5
索引 1:
↓
[a=1] → [h=8] → [i=9] → [j=10] → [k=11]  // 链表长度 5
  • 当链表长度 ≥ TREEIFY_THRESHOLD(默认 8)且数组长度 ≥ MIN_TREEIFY_CAPACITY(默认 64)时,链表转为红黑树。
  • 当前数组长度为 16 < 64,不会转红黑树。
4.3 继续插入更多元素到同一索引

假设继续插入 3 个元素到索引 1,链表长度达到 8

索引 1:
↓
[a=1] → [h=8] → [i=9] → [j=10] → [k=11] → [l=12] → [m=13] → [n=14]

此时数组长度为 16 < 64,仍不会转红黑树。

4.4 触发数组扩容到 64
  • 当元素数量超过 16 * 0.75 = 12 时,数组扩容到 32 → 64。
  • 再次插入元素到索引 1,链表长度 ≥ 8,且数组长度 ≥ 64,触发链表转红黑树:
索引 1:
↓
红黑树节点(原链表已转换)

HashMap 结构总结

操作数组长度链表/红黑树状态
初始化8空数组
插入前 6 个元素8各索引分布链表(长度 1)
插入第 7 个元素16扩容后重新分布链表
插入多个哈希冲突的元素16 → 64链表长度增长,最终转为红黑树(条件满足时)

关键点图解

1. 链表结构
索引 1:
↓
Node1("a",1) → Node2("h",8) → Node3("i",9) → ... 
2. 红黑树结构
索引 1:
↓Node2("h",8)/            \
Node1("a",1)        Node4("j",10)\Node5("k",11)

注意事项

  1. 哈希冲突:不同键的哈希值映射到同一索引时形成链表。
  2. 扩容代价:扩容需重新计算哈希和复制数据,初始化时应预估容量。
  3. 红黑树转换条件:链表长度 ≥8 数组长度 ≥64。
  4. 退化条件:红黑树节点数 ≤6 时退化为链表。

通过这个示例,可以直观看到 HashMap 的动态变化过程,理解其高性能背后的设计机制。


10. 查询示例(get)

当调用 map.get("i") 时,HashMap 会按照以下步骤查找键 "i" 对应的值。我们基于你提供的示例(初始容量为 8,插入多个键后触发扩容和可能的链表转红黑树)逐步分析:

步骤 1:计算键 "i" 的哈希值

  1. 调用 hashCode()
    首先调用 "i".hashCode() 计算原始哈希值。假设 "i".hashCode() 返回 105
  2. 扰动函数优化(Java 8+):
    为了减少哈希冲突,HashMap 会对哈希值进行高 16 位和低 16 位的异或操作:
    java">hash = 105 ^ (105 >>> 16);  // 示例值,实际值可能不同
    
    假设最终哈希值为 hash = 123456(仅示例,具体值取决于实际扰动结果)。

步骤 2:确定桶的索引

  1. 计算索引
    根据当前数组长度 n,计算索引:
    java">index = (n - 1) & hash;
    
    • 扩容前(数组长度为 8):
      java">index = 7 & 123456 = 0;  // 假设计算后索引为 0
      
    • 扩容后(数组长度为 16):
      java">index = 15 & 123456 = 1;  // 假设计算后索引为 1
      
    • 最终索引取决于当前数组长度。假设此时数组已扩容到 16,索引为 1。

步骤 3:遍历链表或红黑树

假设在插入过程中,键 "a""h""i""j""k" 的哈希值均映射到索引 1,且链表已转换为红黑树(当链表长度 ≥8 且数组长度 ≥64 时)。

3.1 查找逻辑
  1. 定位到索引 1
    HashMap 会直接访问数组的索引 1。
  2. 判断数据结构
    • 如果该位置是链表,则从头节点开始遍历,逐个比较键是否等于 "i"
    • 如果该位置是红黑树,则调用红黑树的查找方法,根据键的哈希值和 equals() 方法匹配节点。
3.2 具体操作(以红黑树为例)
  1. 比较哈希值
    从红黑树的根节点开始,比较 "i" 的哈希值与当前节点的哈希值:

    • 如果哈希值小于当前节点,向左子树查找。
    • 如果哈希值大于当前节点,向右子树查找。
    • 如果哈希值相等,则进一步调用 equals() 方法比较键值。
  2. 调用 equals()
    当哈希值匹配时,调用 "i".equals(node.key) 确认键是否一致。

    • 若匹配,返回对应的值。
    • 若不匹配,继续遍历左右子树。

示例查找流程

假设索引 1 的红黑树结构如下(简化表示):

          [h=8] (hash=...)/      \[a=1]        [i=9]/    \[j=10]   [k=11]
  1. 从根节点 h=8 开始
    • "i" 的哈希值可能大于 h=8 的哈希值,向右子树查找。
  2. 找到节点 i=9
    • 哈希值匹配,调用 "i".equals(node.key),确认键一致。
  3. 返回 i=9 的值
    最终返回 9

关键点总结

步骤操作
计算哈希值扰动函数优化减少冲突,得到最终哈希值。
确定索引通过 (n-1) & hash 计算桶的位置,依赖当前数组长度。
遍历数据结构链表(O(n))或红黑树(O(log n))查找,依赖哈希冲突的严重程度。
键匹配先比较哈希值,再调用 equals() 确认键一致性。

注意事项

  1. 哈希冲突的影响
    哈希冲突越多,链表或红黑树的遍历成本越高,因此设计良好的 hashCode() 方法至关重要。
  2. 扩容与性能
    扩容会导致所有键重新哈希,但能减少后续操作的冲突概率。
  3. 红黑树优化
    当哈希冲突严重时(如大量键映射到同一索引),红黑树将查找时间从 O(n) 优化到 O(log n)。

通过这个示例,你可以看到 HashMap 如何高效地通过哈希计算、索引定位和数据结构遍历,快速找到目标键值对。


总结

HashMap 的底层结构是 数组 + 链表/红黑树,通过哈希函数快速定位键值对,使用链地址法解决冲突,并通过动态扩容和红黑树优化性能。理解其内部机制,有助于在实际开发中合理使用并规避潜在问题(如内存泄漏、线程安全问题)。


http://www.ppmy.cn/embedded/170177.html

相关文章

yum源选要配置华为云的源,阿里云用不了的情况

curl -O /etc/yum.repos.d/CentOS-Base.repo https://repo.huaweicloud.com/repository/conf/CentOS-7-reg.repo

2025嵌入式软件开发工程师--音频方向

一、选择题&#xff08;每题3分&#xff0c;共30分&#xff09; 1.以下哪个不是C语言中的关键字?&#xff08; &#xff09; A. int B. Float C. Define D. Return 2.以下代码的输出是: &#xff08; &#xff09; inta 5, b 10; printf("%d“, a b); A. 15 B.16 …

深度学习代码分析——自用

代码来自&#xff1a;https://github.com/ChuHan89/WSSS-Tissue?tabreadme-ov-file 借助了一些人工智能 1_train_stage1.py 代码功能总览 该代码是弱监督语义分割&#xff08;WSSS&#xff09;流程的 Stage1 训练与测试脚本&#xff0c;核心任务是通过 多标签分类模型 生成…

【数据挖掘]Ndarray数组的创建

在 NumPy 中&#xff0c;ndarray&#xff08;N-dimensional array&#xff09;是最核心的数据结构&#xff0c;创建 ndarray 数组的方式有多种&#xff0c;主要包括以下几类&#xff1a; 目录 1. 通过列表或元组创建 2. 使用 NumPy 内置的创建函数 &#xff08;1&#xff0…

详解DeepSeek模型底层原理及和ChatGPT区别点

一、DeepSeek大模型原理 架构基础 DeepSeek基于Transformer架构,Transformer架构主要由编码器和解码器组成,在自然语言处理任务中,通常使用的是Transformer的解码器部分。它的核心是自注意力机制(Self - Attention),这个机制允许模型在处理输入序列时,关注序列中不同位…

DeepSeek集成到VScode工具,让编程更高效

DeepSeek与VScode的强强联合&#xff0c;为编程效率树立了新标杆。 DeepSeek&#xff0c;一款卓越的代码搜索引擎&#xff0c;以其精准的索引和高速的检索能力&#xff0c;助力开发者在浩瀚的代码海洋中迅速定位关键信息。 集成至VScode后&#xff0c;开发者无需离开熟悉的编辑…

Excel文件中物件PPT文档如何保存到本地

以下是Excel中嵌入的PPT文档保存到本地的详细方法&#xff0c;综合了多种适用场景的解决方案&#xff1a; 方法一&#xff1a;直接通过对象功能另存为 定位嵌入的PPT对象 在Excel中双击打开嵌入的PPT文档&#xff0c;进入编辑模式后&#xff0c;右键点击PPT对象边框&#xff0…

【分布式】Hadoop完全分布式的搭建(零基础)

Hadoop完全分布式的搭建 环境准备&#xff1a; &#xff08;1&#xff09;VMware Workstation Pro17&#xff08;其他也可&#xff09; &#xff08;2&#xff09;Centos7 &#xff08;3&#xff09;FinalShell &#xff08;一&#xff09;模型机配置 0****&#xff09;安…