JDK 8 的HashMap扩容源代码分析

server/2025/2/5 0:55:33/
java">final Node<K,V>[] resize() {Node<K,V>[] oldTab = table; // 获取原来的数组 tableint oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取数组长度 oldCapint oldThr = threshold; // 获取阈值 oldThrint newCap, newThr = 0;if (oldCap > 0) { // 如果原来的数组 table 不为空if (oldCap >= MAXIMUM_CAPACITY) { // 超过最大值就不再扩充了,就只好随你碰撞去吧threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // 没超过最大值,就扩充为原来的2倍oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else { // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 计算新的 resize 上限if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr; // 将新阈值赋值给成员变量 threshold@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新数组 newTabtable = newTab; // 将新数组 newTab 赋值给成员变量 tableif (oldTab != null) { // 如果旧数组 oldTab 不为空for (int j = 0; j < oldCap; ++j) { // 遍历旧数组的每个元素Node<K,V> e;if ((e = oldTab[j]) != null) { // 如果该元素不为空oldTab[j] = null; // 将旧数组中该位置的元素置为 null,以便垃圾回收if (e.next == null) // 如果该元素没有冲突newTab[e.hash & (newCap - 1)] = e; // 直接将该元素放入新数组else if (e instanceof TreeNode) // 如果该元素是树节点((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 将该树节点分裂成两个链表else { // 如果该元素是链表Node<K,V> loHead = null, loTail = null; // 低位链表的头结点和尾结点Node<K,V> hiHead = null, hiTail = null; // 高位链表的头结点和尾结点Node<K,V> next;do { // 遍历该链表next = e.next;if ((e.hash & oldCap) == 0) { // 如果该元素在低位链表中if (loTail == null) // 如果低位链表还没有结点loHead = e; // 将该元素作为低位链表的头结点elseloTail.next = e; // 如果低位链表已经有结点,将该元素加入低位链表的尾部loTail = e; // 更新低位链表的尾结点}else { // 如果该元素在高位链表中if (hiTail == null) // 如果高位链表还没有结点hiHead = e; // 将该元素作为高位链表的头结点elsehiTail.next = e; // 如果高位链表已经有结点,将该元素加入高位链表的尾部hiTail = e; // 更新高位链表的尾结点}} while ((e = next) != null); //if (loTail != null) { // 如果低位链表不为空loTail.next = null; // 将低位链表的尾结点指向 null,以便垃圾回收newTab[j] = loHead; // 将低位链表作为新数组对应位置的元素}if (hiTail != null) { // 如果高位链表不为空hiTail.next = null; // 将高位链表的尾结点指向 null,以便垃圾回收newTab[j + oldCap] = hiHead; // 将高位链表作为新数组对应位置的元素}}}}}return newTab; // 返回新数组
}

这段代码是 Java 中 HashMap 的 扩容(resize) 方法,用于在哈希表容量不足时扩大数组长度,并重新分布所有元素。以下是逐步分析:


1. 核心目标

  • 扩容:当哈希表中的元素数量超过阈值(threshold = capacity * loadFactor)时,扩容数组以减少哈希冲突,保证查询效率。

  • 重分布:将旧数组中的元素重新分配到新数组中,确保哈希表的均匀分布。


2. 代码分步解析

步骤 1:计算新容量和新阈值

java">Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;if (oldCap > 0) {// 旧容量已达最大值,无法扩容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 新容量 = 旧容量 * 2,新阈值 = 旧阈值 * 2else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {newThr = oldThr << 1;}
} else if (oldThr > 0) { // 初始化时手动指定了容量(通过阈值)newCap = oldThr;
} else { // 默认初始化newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}// 计算新阈值(如果未在之前的逻辑中设置)
if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
  • 规则

    1. 默认扩容为原容量的 2 倍(newCap = oldCap << 1)。

    2. 新阈值 = 新容量 × 负载因子(如 0.75)。

    3. 若旧容量已达最大值(MAXIMUM_CAPACITY),则不再扩容。


步骤 2:创建新数组
java">Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 更新哈希表的数组引用
  • 创建一个新数组 newTab,容量为 newCap,并将其赋值给哈希表的 table 字段。


步骤 3:迁移旧数据到新数组
java">
if (oldTab != null) {for (int j = 0; j < oldCap; ++j) { // 遍历旧数组的每个位置(桶)Node<K,V> e;if ((e = oldTab[j]) != null) { // 如果当前位置有元素oldTab[j] = null; // 清空旧位置,帮助垃圾回收// 情况 1:单个节点(无冲突)if (e.next == null) {newTab[e.hash & (newCap - 1)] = e; // 直接计算新位置}// 情况 2:红黑树节点else if (e instanceof TreeNode) {((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 分裂树}// 情况 3:链表节点else { // 定义低位链表和高位链表的头尾指针Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do { // 遍历链表next = e.next;// 判断节点属于低位链表还是高位链表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;}// 将高位链表放入新数组的 [原索引 + 旧容量] 位置if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}
}
关键点:
  1. 单个节点:直接计算新位置 e.hash & (newCap - 1)

  2. 红黑树节点:调用 split 方法将树拆分为两个链表,再判断是否需要退化为链表。

  3. 链表节点

    • 通过 (e.hash & oldCap) 判断节点属于 低位链表(原索引位置)还是 高位链表(原索引 + 旧容量位置)。

      注:扩容为原来的一倍以后,链表索引只可能是低位链表(原索引位置)或者高位链表(原索引 + 旧容量位置)。
    • 低位链表:哈希值的最高位是 0,新索引与原索引相同。

    • 高位链表:哈希值的最高位是 1,新索引 = 原索引 + 旧容量。

    • 尾插法:保持链表的原有顺序(避免多线程下的死循环问题)。


3. 设计思想

  1. 均匀分布:通过扩容减少哈希冲突,保证查询效率为 O(1)。

  2. 高效迁移

    • 利用位运算 (e.hash & oldCap) 快速判断新位置,避免重新计算哈希值。

    • 拆分链表为高低位两部分,直接分配到新数组的对应位置。

  3. 兼容性

    • 处理单节点、链表、红黑树三种情况。

    • 在链表迁移时使用尾插法,避免多线程环境下的链表死循环问题。


4. 举例说明

假设旧容量为 8,某个桶的链表为 A → B → C,哈希值分别为:

  • A.hash & 8 = 0 → 低位链表

  • B.hash & 8 = 8 → 高位链表

  • C.hash & 8 = 0 → 低位链表

迁移后:

  • 低位链表 A → C 放入新数组的索引 j(原位置)。

  • 高位链表 B 放入新数组的索引 j + 8(旧容量 + 原位置)。


http://www.ppmy.cn/server/165025.html

相关文章

前缀和多种基础

前缀和加法 #include<iostream> #include<algorithm> using namespace std; typedef long long ll; int n; const int N 1e310; int arr[N]; int pre[N]; int org[N]; int main(void) {cin >> n;for(int i 1 ; i < n ; i){cin >> arr[i];pre[i] …

机器学习day5

自定义数据集 使用tensorflow框架实现逻辑回归并保存模型&#xff0c;然后保存模型后再加载模型进行预测 代码 import tensorflow as tf import numpy as np# 1. 自定义数据集 data [[-0.5, 7.7], [1.8, 98.5], [0.9, 57.8], [0.4, 39.2], [-1.4, -15.7], [-1.4, -37.3], [-1…

SpringBoot整合Mybatis|入门级增删改查|2025

SpringBoot整合Mybatis 文章目录 SpringBoot整合Mybatis1. 新建User表2. 初始化项目2.1 新建项目2.2 配置数据库连接2.3 完善项目的架子 3. 正式开始3.1 新增用户3.2 根据邮箱查询3.4 改密码 和 删除用户3.5 用xml再写一遍 4. 进阶 1. 新建User表 CREATE DATABASE mybatis_dem…

音视频入门基础:RTP专题(7)——RTP协议简介

一、引言 本文对RTP协议进行简介。在简介之前&#xff0c;请各位先下载RTP的官方文档《RFC 3550》和《RFC 3551》。《RFC 3550》总共有89页&#xff0c;《RFC 3551》总共有44页。本文下面所说的“页数”是指在pdf阅读器中显示的页数&#xff1a; 二、RTP协议简介 根据《RFC 35…

nvm的安装和使用

打开地址下载 https://github.com/coreybutler/nvm-windows/releases 推荐下载&#xff0c;nvm-setup.zip 这个。可能有的教程会让下载nvm-noinstall.zip 。noinstall确实下载之后不用安装&#xff0c;但是要自己配置setting.txt文件&#xff0c;以及环境变量 。 安装nvm 在电…

深度学习-98-大语言模型LLM之基于langchain的代理create_react_agent工具

文章目录 1 Agent代理1.1 代理的分类1.2 ReAct和Structured chat2 代理应用ReAct2.1 创建工具2.1.1 嵌入模型2.1.2 创建检索器2.1.3 测试检索结果2.1.4 创建工具列表2.2 初始化大模型2.3 创建Agent2.4 运行Agent3 参考附录1 Agent代理 Agent代理的核心思想是使用语言模型来选择…

数据结构 树2

文章目录 前言 一&#xff0c;二叉搜索树的高度 二&#xff0c;广度优先VS深度优先 三&#xff0c;广度优先的代码实现 四&#xff0c;深度优先代码实现 五&#xff0c;判断是否为二叉搜索树 六&#xff0c;删除一个节点 七&#xff0c;二叉收索树的中序后续节点 总结 …

Go学习:格式化输入输出

目录 1. 输出 2. 输入 1. 输出 常用格式&#xff1a; 格式说明%d整型格式%s字符串格式%c字符格式%f浮点数格式%T操作变量所属类型%v自动匹配格式输出 简单示例代码&#xff1a; package mainimport "fmt"func main() {a : 10b : "abc"c : ad : 3.14/…