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

学习笔记05——HashMap实现原理及源码解析(JDK8)

HashMap实现原理及源码解析(JDK8)


一、核心设计思想
  1. 数组+链表+红黑树:桶数组存储Node节点,哈希冲突时形成链表,链表长度≥8且桶数量≥64时转红黑树
  2. 扰动函数(h = key.hashCode()) ^ (h >>> 16) 消除高位变化的影响
  3. 懒加载:首次put时初始化数组
  4. 负载因子:默认0.75,空间与时间的权衡

二、put()方法源码全解析

代码路径:java.util.HashMap#put

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

核心方法putVal()流程

  1. 初始化检测if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
  2. 计算桶下标(n - 1) & hash
  3. 处理空桶:直接创建新节点
  4. 处理哈希冲突
    • 红黑树节点:putTreeVal()
    • 链表节点:遍历至尾节点插入,同时检测链表长度
  5. 值替换检测if (e != null) { ... return oldValue; }
  6. 扩容检测if (++size > threshold) resize();

源码解读

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; // 当前哈希桶数组Node<K,V> p;      // 目标桶的第一个节点int n, i;         // n: 数组长度,i: 计算出的桶下标// ------------------------ 1. 初始化检测 ------------------------if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length; // 首次插入触发初始化// ------------------------ 2. 计算桶下标 ------------------------i = (n - 1) & hash; // 等价于 hash % n(n是2的幂时的优化)p = tab[i]; // 获取目标桶的第一个节点// ------------------------ 3. 处理空桶 ------------------------if (p == null)tab[i] = newNode(hash, key, value, null); // 直接插入新节点else {Node<K,V> e; // 用于记录已存在的节点(若找到相同key)K k;// ------------------- 4.1 处理首节点匹配 -------------------if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p; // 记录已存在的节点// ------------------- 4.2 处理红黑树节点 -------------------else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// ------------------- 4.3 处理链表节点 ---------------------else {for (int binCount = 0; ; ++binCount) {// 到达链表尾部if ((e = p.next) == null) {p.next = newNode(hash, key, value, null); // 尾插法// 检查是否触发树化if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 发现重复keyif (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e; // 继续遍历}}// ------------------- 5. 处理值替换 ------------------------if (e != null) { // 存在重复keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value; // 覆盖旧值afterNodeAccess(e);  // LinkedHashMap回调return oldValue;    // 返回旧值}}++modCount; // 修改计数器(用于快速失败机制)// ------------------------ 6. 扩容检测 ------------------------if (++size > threshold)resize(); afterNodeInsertion(evict); // LinkedHashMap回调return null; // 插入成功返回null
}

三、扩容机制深度剖析

触发条件:元素数量 > 容量 × 负载因子

resize()核心步骤

  1. 计算新容量:原容量×2(保证始终为2的幂)
  2. 创建新数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  3. 数据迁移
    • 单节点:newTab[e.hash & (newCap - 1)] = e;
    • 红黑树:split()方法处理
    • 链表:拆分为低位链和高位链(利用(e.hash & oldCap) == 0判断)

扩容优化点

  • 无需重新计算哈希:通过位运算快速确定新位置
  • 链表保持原始顺序(JDK8改进,避免循环链表问题)

源码解析

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;          // 旧数组int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧容量int oldThr = threshold;              // 旧阈值int newCap, newThr = 0;              // 新容量、新阈值// -------------------- 1. 计算新容量和阈值 --------------------if (oldCap > 0) { // 扩容场景// 容量已达最大值(2^30),不再扩容if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE;return oldTab;}// 新容量 = 旧容量 * 2else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // 阈值 * 2}else if (oldThr > 0) // 初始化场景(指定初始容量)newCap = oldThr; // 使用构造方法中计算的阈值作为初始容量else { // 无参构造初始化(默认容量16)newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 处理新阈值为0的情况(兜底逻辑)if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?(int)ft : Integer.MAX_VALUE;}threshold = newThr; // 更新全局阈值// -------------------- 2. 创建新数组 --------------------Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab; // 更新全局数组引用// -------------------- 3. 数据迁移(旧数组→新数组) --------------------if (oldTab != null) {for (int j = 0; j < oldCap; ++j) { // 遍历旧数组的每个桶Node<K,V> e;if ((e = oldTab[j]) != null) { // 桶中有数据oldTab[j] = null; // 清空旧桶(帮助GC)// ----------- 3.1 处理单节点 -----------if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 直接计算新位置// ----------- 3.2 处理红黑树节点 -----------else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// ----------- 3.3 处理链表节点(关键优化) -----------else { Node<K,V> loHead = null, loTail = null; // 低位链表(原索引)Node<K,V> hiHead = null, hiTail = null; // 高位链表(原索引+oldCap)do {Node<K,V> 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;newTab[j] = loHead; // 原索引位置}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead; // 新索引位置}}}}}return newTab;
}

核心设计亮点

  1. 避免重新计算哈希
    通过 (e.hash & oldCap) == 0 判断节点位置,时间复杂度从 O(n) 降为 O(1)
  2. 高低位链表拆分
    • 低位链表:保持原索引位置 j
    • 高位链表:新索引位置为 j + oldCap
    • 顺序保留:JDK8 使用尾插法保持链表原始顺序,避免循环链表问题(对比 JDK7 头插法)
  3. 红黑树退化机制
    当红黑树节点数 ≤6 时退化为链表,避免空间浪费
  4. 延迟初始化
    首次调用 put() 时才会初始化数组(懒加载)

Q1:为什么HashMap扩容是2倍?

  • 保证容量始终为2的幂,使得 (n - 1) & hash 等效于取模运算,且位运算效率更高
  • 扩容时可通过位运算快速确定新位置(无需重新计算哈希)

Q2:JDK8扩容如何避免死循环?

  • JDK7 使用头插法,多线程扩容可能导致循环链表
  • JDK8 改为尾插法,并保持链表原始顺序,彻底解决该问题

Q3:链表拆分的数学原理是什么?
假设旧容量为 oldCap = 16(二进制 10000):

  • hash & oldCap == 0 → 节点新位置 = 原位置
  • hash & oldCap != 0 → 节点新位置 = 原位置 + oldCap
    本质是通过哈希值的第 log2(oldCap) 位是否为1决定位置

Q4:多线程环境下resize()的风险

  • 数据丢失:两个线程同时触发扩容可能导致部分节点丢失
  • 链表断裂:并发修改可能导致链表结构损坏
  • 解决方案:使用 ConcurrentHashMap
四、关键代码
1.哈希扰动函数
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 设计目的:将高16位与低16位异或,减少低位相同导致的哈希碰撞
  • 示例:若 hashCode()0x12345678,扰动后为 0x12345678 ^ 0x00001234 = 0x1234444C
2.树化逻辑
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize(); // 优先扩容而不是树化else if ((e = tab[index = (n - 1) & hash]) != null) {// 将链表转为红黑树TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);}
}
  • 树化条件:链表长度 ≥8 桶数量 ≥64(MIN_TREEIFY_CAPACITY
  • 设计考量:避免在小表时过早树化,空间利用率更重要
3.红黑树插入

putTreeVal() 方法中:

  1. 从根节点开始查找插入位置
  2. 比较哈希值和键对象
  3. 插入后通过 balanceInsertion() 进行红黑树平衡操作
4.核心设计亮点
  1. 尾插法 vs 头插法
    • JDK7 使用头插法,多线程扩容可能产生循环链表
    • JDK8 改为尾插法,解决循环链表问题,保证插入顺序
  2. 延迟树化策略
    • 优先扩容而不是立即树化(当桶数量 <64 时)
    • 树节点占用空间是普通节点的2倍,权衡空间与时间
  3. 高低位链表拆分
    • 扩容时通过 (e.hash & oldCap) == 0 判断节点位置
    • 示例:原容量16(二进制10000),哈希值为10101的节点在扩容后会分配到新位置 10101 & 31(11111) = 21

面试回答话术模板

面试官:请说明HashMap的实现原理

回答结构

  1. 总体结构:“HashMap采用数组+链表+红黑树的存储结构,当链表长度超过8且桶数量达到64时,链表会转换为红黑树优化查询效率”
  2. 哈希计算:“通过二次扰动函数处理键的hashCode,使哈希分布更均匀”
  3. put流程
    • 计算桶下标 → 处理空桶/冲突 → 链表/树操作 → 扩容检测
    • 强调尾插法、树化阈值、modCount记录
  4. 扩容机制
    • 2倍扩容保证容量始终为2的幂
    • 数据迁移时的高低位链表拆分算法
  5. 线程安全:“HashMap非线程安全,多线程环境下建议使用ConcurrentHashMap”,原因是:1.两个线程同时put时可能会覆盖彼此的修改;2.jdk7的头插法扩容时产生循环链表,发生死循环;
  6. 延伸补充(可选):
    • 与HashTable的区别(null键支持、同步机制)
    • 负载因子取值依据(0.75的时间空间平衡点)

加分技巧

  • 手绘数据迁移示意图(高低位链表拆分)
  • 举例说明哈希碰撞场景
  • 对比JDK7的头插法实现差异(循环链表问题)
  • 分析树化阈值的统计学依据(泊松分布)

示例回答:

“HashMap通过数组存储Node节点解决快速定位问题,使用链表和红黑树处理哈希冲突。当执行put操作时,首先计算键的扰动哈希值确定桶位置。若发生哈希冲突,JDK8采用尾插法维护链表,当链表长度达到树化阈值时会转换结构提升查询效率。扩容时采用2倍扩容策略,通过位运算快速重新分配节点位置,这个设计避免了重新计算哈希的开销。需要注意的是,HashMap不是线程安全的,多线程put可能导致数据丢失或死循环(JDK7版本)。”


建议结合手写伪代码/流程图进行说明,展现对底层实现的深入理解。同时可准备时间/空间复杂度分析(理想情况下O(1)操作)作为补充。

相关文章:

学习笔记05——HashMap实现原理及源码解析(JDK8)

HashMap实现原理及源码解析&#xff08;JDK8&#xff09; 一、核心设计思想 数组链表红黑树&#xff1a;桶数组存储Node节点&#xff0c;哈希冲突时形成链表&#xff0c;链表长度≥8且桶数量≥64时转红黑树扰动函数&#xff1a;(h key.hashCode()) ^ (h >>> 16) 消除…...

React面试(一)

文章目录 1.vue和react有什么异同2.useEffect中为什么不能使用异步3.useEffect和useLayoutEffect的区别4.react的生命周期5.state和prop的区别6.受控组件和非受控组件7.为什么react16之后不把事件挂载到document上了8.讲一下react的hoc&#xff0c;它可以用来做什么&#xff1f…...

Redis分布式缓存面试题

为什么使用分布式缓存&#xff1f; 1. 提升性能 降低延迟&#xff1a;将数据缓存在离应用更近的地方&#xff0c;减少数据访问时间。减轻数据库压力&#xff1a;缓存频繁访问的数据&#xff0c;减少对后端数据库的请求&#xff0c;提升系统响应速度。 2. 扩展性 水平扩展&a…...

AI 编码 2.0 分析、思考与探索实践:从 Cursor Composer 到 AutoDev Sketch

在周末的公司【AI4SE 效能革命与实践&#xff1a;软件研发的未来已来】直播里&#xff0c;我分享了《AI编码工具 2.0 从 Cursor 到 AutoDev Composer》主题演讲&#xff0c;分享了 AI 编码工具 2.0 的核心、我们的思考、以及我们的 AI 编码工具 2.0 探索实践。 在这篇文章中&am…...

图扑 HT for Web 总线式拓扑图的可视化实现

在图形用户界面&#xff08;GUI&#xff09;设计中&#xff0c;自定义连线技术不仅提升了用户体验&#xff0c;还为复杂数据可视化开辟了新的可能性。该功能点允许用户灵活地在界面元素之间创建视觉连接&#xff0c;使流程图、思维导图和网络拓扑图等信息呈现更加直观和动态。 …...

domain 网络安全 网络安全域

&#x1f345; 点击文末小卡片 &#xff0c;免费获取网络安全全套资料&#xff0c;资料在手&#xff0c;涨薪更快 文章目录 1、域的概述 1.1、工作组与域1.2、域的特点1.3、域的组成1.4、域的部署概述1.5、活动目录1.6、组策略GPO 2、域的部署实验 2.1、建立局域网&#xf…...

IDEA 2024.1 最新永久可用(亲测有效)

今年idea发布了2024.1版本&#xff0c;这个版本带来了一系列令人兴奋的新功能和改进。最引人注目的是集成了更先进的 AI 助手&#xff0c;它现在能够提供更复杂的代码辅助功能&#xff0c;如代码自动补全、智能代码审查等&#xff0c;极大地提升了开发效率。此外&#xff0c;用…...

android计算屏幕尺寸dpi

说明&#xff1a; 我计划用一个Android程序&#xff0c;打印出平板屏幕的尺寸&#xff0c;大小&#xff0c;dpi等参数信息 效果图&#xff1a; 分辨率: 1280x752DPI: 213物理尺寸(英寸): 对角线 9.4step1: package com.example.myapplication;import android.os.Bundle; impor…...

deepseek-r1-centos-本地服务器配置方法

参考&#xff1a; 纯小白 Centos 部署DeepSeek指南_centos部署deepseek-CSDN博客 https://blog.csdn.net/xingxin550/article/details/145574080 手把手教大家如何在Centos7系统中安装Deepseek&#xff0c;一文搞定_centos部署deepseek-CSDN博客 https://blog.csdn.net/soso67…...

nginx 配置https

参考文档&#xff1a;nginx 文档 -- nginx官网|nginx下载安装|nginx配置|nginx教程 配置 HTTPS 服务器 HTTPS 服务器优化 SSL 证书链 单个 HTTP/HTTPS 服务器 基于名称的 HTTPS 服务器 具有多个名称 的 SSL 证书 服务器名称指示 兼容性 要配置 HTTPS 服务器&#xff0c;ssl…...

mapbox添加自定义图片绑定点击事件,弹窗为自定义组件

一、首先构建根据后端返回的数据构建geojson格式的数据&#xff0c;点位的geojson数据格式&#xff1a; {"type": "FeatureCollection","features": [{"type": "Feature","geometry": {"type": "…...

【Python爬虫(84)】当强化学习邂逅Python爬虫:解锁高效抓取新姿势

【Python爬虫】专栏简介:本专栏是 Python 爬虫领域的集大成之作,共 100 章节。从 Python 基础语法、爬虫入门知识讲起,深入探讨反爬虫、多线程、分布式等进阶技术。以大量实例为支撑,覆盖网页、图片、音频等各类数据爬取,还涉及数据处理与分析。无论是新手小白还是进阶开发…...

车载DoIP诊断框架 --- 连接 DoIP ECU/车辆的故障排除

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身…...

嵌入式开发:傅里叶变换(4):在 STM32上面实现FFT(基于STM32L071KZT6 HAL库+DSP库)

目录 步骤 1&#xff1a;准备工作 步骤 2&#xff1a;创建 Keil 项目&#xff0c;并配置工程 步骤 3&#xff1a;在MDK工程上添加 CMSIS-DSP 库 步骤 5&#xff1a;编写代码 步骤 6&#xff1a;配置时钟和优化 步骤 7&#xff1a;调试与验证 步骤 8&#xff1a;优化和调…...

Uniapp 小程序接口封装与使用

深入理解 Uniapp 小程序接口封装与使用 在 Uniapp 小程序开发中&#xff0c;接口请求是获取和交互数据的关键部分。合理地封装接口不仅能提高代码的可维护性&#xff0c;还能增强项目的健壮性。今天&#xff0c;我们就来详细探讨一下如何在 Uniapp 中进行接口封装、引入以及使…...

Python入门 — 类

面向对象编程中&#xff0c;编写表示现实世界中的事物和情景的类&#xff08;class&#xff09;&#xff0c;并基于这些类来创建对象&#xff08;object&#xff09;。根据类来创建对象称为实例化&#xff0c;这样就可以使用类的实例&#xff08;instance&#xff09; 一、创建…...

vscode/cursor+godot C#中使用socketIO

在 Visual Studio Code(VS Code)中安装 NuGet 包&#xff08;例如SocketIOClient&#xff09;&#xff0c;你可以通过以下几种方法&#xff1a; 方法 1&#xff1a;使用dotnet cli 打开终端&#xff1a;在 VS Code 中按下Ctrl 或者通过菜单View -> Terminal打开终端。 导…...

应用的负载均衡

概述 负载均衡&#xff08;Load Balancing&#xff09; 调度后方的多台机器&#xff0c;以统一的接口对外提供服务&#xff0c;承担此职责的技术组件被称为“负载均衡”。 负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。负载均衡是计算机网络中一种用于优化资源利…...

Kubernetes与Docker:区别与优劣总结

在云原生技术栈中&#xff0c;Docker和Kubernetes是两大核心工具&#xff0c;但它们的功能定位和使用场景截然不同。本文将从技术原理、架构设计、功能特性及适用场景等角度&#xff0c;深入分析两者的区别与优劣&#xff0c;并结合实际应用场景说明如何协同使用。 一、核心技术…...

区块链仿真工具SimBlock使用

1. Environment requirements SimBlock 可以在 Windows、MacOS、Ubuntu Linux 或任何支持 Java 的 Unix 平台上运行。 它需要以下版本的 JDK 和 Gradle。 请注意&#xff0c;SimBlock 的仓库中包含 Gradle Wrapper&#xff0c;因此您也可以自动安装 Gradle&#xff08;我们稍…...

面试八股文--数据库基础知识总结(2) MySQL

本文介绍关于MySQL的相关面试知识 一、关系型数据库 1、定义 关系型数据库&#xff08;Relational Database&#xff09;是一种基于关系模型的数据库管理系统&#xff08;DBMS&#xff09;&#xff0c;它将数据存储在表格&#xff08;表&#xff09;中&#xff0c;并通过表格…...

LeetCode 1472.设计浏览器历史记录:一个数组完成模拟,单次操作均O(1)

【LetMeFly】1472.设计浏览器历史记录&#xff1a;一个数组完成模拟&#xff0c;单次操作均O(1) 力扣题目链接&#xff1a;https://leetcode.cn/problems/design-browser-history/ 你有一个只支持单个标签页的 浏览器 &#xff0c;最开始你浏览的网页是 homepage &#xff0c…...

江协科技/江科大-51单片机入门教程——P[1-3] 单片机及开发板介绍

前言&#xff1a;本节主要的任务是了解一下 51 单片机和所用的普中51开发板。 目录 一、单片机介绍 二、单片机的应用领域 三、STC89C52单片机 四、命名规则 五、单片机内部拆解 六、单片机内部结构图 七、单片机管脚图 八、单片机最小系统 九、开发板介绍 十、开发…...

【Uniapp-Vue3】导入uni-id用户体系

在uniapp官网的uniCloud中下载uni-id用户体系 或者直接进入加载&#xff0c;下载地址&#xff1a;uni-id-pages - DCloud 插件市场 进入以后下载插件&#xff0c;打开HbuilderX 选中项目&#xff0c;点击确定 点击跳过 点击合并 右键uniCloud文件夹下的database文件夹&#x…...

如何免费使用稳定的deepseek

0、背景&#xff1a; 在AI辅助工作中&#xff0c;除了使用cursor做编程外&#xff0c;使用deepseek R1进行问题分析、数据分析、代码分析效果非常好。现在我经常会去拿行业信息、遇到的问题等去咨询R1&#xff0c;也给了自己不少启示。但是由于官网稳定性很差&#xff0c;很多…...

基于 ‌MySQL 数据库‌对三级视图(用户视图、DBA视图、内部视图)的详细解释

基于 ‌MySQL 数据库‌对三级视图&#xff08;用户视图、DBA视图、内部视图&#xff09;的详细解释&#xff0c;结合理论与实际操作说明&#xff1a; 一、三级视图核心概念 数据库的三级视图是 ANSI/SPARC 体系结构的核心思想&#xff0c;MySQL 的实现逻辑如下&#xff1a; …...

easyexcel和poi同时存在版本问题,使用easyexcel导出excel设置日期格式

这两天在使用easyexcel导出excel的时候日期格式全都是字符串导致导出的excel列无法筛选 后来调整了一下终于弄好了&#xff0c;看一下最终效果 这里涉及到easyexcel和poi版本冲突的问题&#xff0c;一直没搞定&#xff0c;最后狠下心来把所有的都升级到了最新版&#xff0c;然…...

git 国内源

git config --global url.“https://hub.fastgit.xyz/”.insteadOf “https://github.com/” git config --global url.“https://hub.fastgit.xyz/”.insteadOf “git://github.com/” 取消 FastGit 代理: git config --global --unset url.“https://hub.fastgit.xyz/”.in…...

Spring Boot @Async 注解深度指南

Spring Boot Async 注解深度指南 一、核心使用要点 启用异步支持 必须在启动类或配置类添加 EnableAsync&#xff0c;否则异步不生效。 SpringBootApplication EnableAsync public class Application { ... }线程池配置 默认问题&#xff1a;Spring 默认使用 SimpleAsyncTaskEx…...

Facebook Instant Game:即时游戏的新时代

一、Facebook Instant Game 简介 Facebook Instant Game(即时游戏)是一种基于 HTML5 技术的轻量级游戏平台&#xff0c;允许用户无需下载即可在Facebook Messenger 和 News Feed 中直接游玩。这种游戏模式降低了用户的进入门槛&#xff0c;使得游戏可以迅速传播&#xff0c;极…...