本文主要包含:HashMap 插入过程、扩容过程、查询过程和删除过程的源码可视化

文章对应的视频连接:https://www.bilibili.com/video/BV1wM3KzaE3d/

1. 操作流程

1.1. 插入过程(put(K key, V value)

插入流程主要涉及四种操作:扩容(首层扩容和阈值扩容)、单节点插入(无哈希冲突的情况)、链表遍历插入(冲突节点不超8个的情况)、红黑树插入。

插入节点的全流程图:

1.2. 扩容过程(resize()

扩容条件、扩容涉及到的链表挂载、链表树化、树转链表等等,都在前一篇四次扩容的文章中讲到,渐进式的学习HashMap扩容。

HashMap扩容源码可视化

文章链接:https://mp.weixin.qq.com/s/J3kU51hb-GcM4Rsp7QCIFw

视频链接:https://www.bilibili.com/video/BV1wM3KzaE3d/

1.3. 查询过程(get(Object key)

  1. 空表或 key == null :立即返回 nullnull key 存储在索引 0)。

  2. 计算 hash、下标 i

  3. 遍历

    • 如果 table[i] 为单链表,逐节点比较 hashkey.equals

    • 如果为红黑树,调用 TreeNodegetTreeNode,按树结构快速查找(对数复杂度)。

查找元素全流程图:

1.4. 删除过程(remove(Object key)

最后将展示单链表的节点删除和红黑树节点的删除可视化过程。

  1. 计算 hash、下标 i

  2. 定位到槽位的链表或树,找到目标节点。

  3. 单链表:直接断链跳过;红黑树:调用 removeTreeNode 完成删除。

  4. size--,更新 modCount,返回被删节点的值。

删除链表节点

跟普通链表删除节点一样简单,下面直接通过动图来理解

删除红黑树节点

对于链表的删除处理是很简单很好理解,但是对于红黑树的删除就会比较复杂。在HashMap中,红黑树节点删除的可视化:

关键步骤大致分为四步:寻找替换节点、进入待删除状态、红黑树平衡调整和最终删除节点。

最主要的两步源码如下,其次就是红黑树数据结构删除过程的理解:

寻找替换:寻找中序后继节点作为替换节点。比如:红黑树左中右序为1、2、3、4,删除了2节点,那么就找3节点作为替换节点;如果删除3节点,那么就找4节点作为替换。对应的源代码如下

if (pl != null && pr != null) {
// 如果左右都不为空,找中序的后继节点替换,(右子树最靠左的节点)
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null)
s = sl; ...
}

删除黑节点树平衡

删除的主要源码如下,已为每一行源码附上注释。

// map: 当前所在的 HashMap 实例
// tab: 哈希表数组(即 table)
// movable=true
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
// 如果表为 null 或长度为 0,直接返回(无操作)
if (tab == null || (n = tab.length) == 0)
return;
// 用哈希值定位当前节点所在的桶 index
int index = (n - 1) & hash;
// 获取根节点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
// 从链表中断开当前节点(this),红黑树同时是双向链表这点必须知道,所以才有维护链表的操作
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
// 如果断链后没有剩余节点(即只有当前一个节点),直接返回
if (first == null)
return; if (root.parent != null)
root = root.root();
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
// 红黑树删除操作
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
// 处理删除节点p 同时有左右子节点的情况
if (pl != null && pr != null) { // 如果左右都不为空,找中序的后继替换,(右子树最靠左的节点)
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl; // 节点颜色交换
boolean c = s.red; s.red = p.red; p.red = c; // swap colors // 交换结构:把后继节点换上来
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
// p 是 s 的直接父节点
if (s == pr) {
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
// 调整左子树与父指针:p 的左右清空(它即将被删),s 左右指针都设置好(接替 p)
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s; // s 接替 p 成为新的 root 的子节点
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s; // 设置替换节点
if (sr != null)
replacement = sr;
else
replacement = p;
} // 处理删除节点p 有一个或没有子节点情况
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p; // 让 replacement 替换掉 删除节点p 的位置
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
} // 如果删除节点p 是黑节点,需要平衡红黑树
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement); // 如果 p 等于 replacement,说明删除的节点是叶子节点,断开叶子节点
if (replacement == p) {
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
} // 将新的 root 移动到链表最前(优化访问)
if (movable)
moveRootToFront(tab, r);
}

2. 性能与并发考虑

时间复杂度

操作 平均时间复杂度 最坏时间复杂度 备注
get O(1) O(log n) 红黑树查找最坏 O(log n)
put O(1) O(log n) 链表树化后插树最坏 O(log n)
remove O(1) O(log n) 红黑树删除维护平衡 O(log n)
resize O(1) O(n) 摊销成本后每次插入 O(1)
遍历全部元素 O(n) O(n)

并发风险

HashMap 非线程安全,在多线程无外部同步时可能出现数据丢失或死循环(扩容时环路)。

多线程并发场景推荐使用 ConcurrentHashMap,或者对 HashMap 外层加锁(如 Collections.synchronizedMap,串行效率低)

3. 在 HashMap 中红黑树同时是双向链表?

链表节点(未树化)Node<K,V> 类型,只包含:

Node<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}

单向链表

树化节点(TreeNode):扩展自 Node<K,V>,添加了:

TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // 这是额外的双向链表字段
boolean red;
}

双向链表 + 红黑树结构

3.1. 红黑树根节点始终是双向链表的头节点

这里所说的双向链表结构指的是:在同一个桶中的红黑树。

在不同的桶中,红黑树之间是没有联系的,也不存在双向链表。

moveRootToFront 这个方法有两个作用

  • 更新数组桶指向新的根节点

  • 更新根节点为双向链表的头节点,并将旧的根节点作为下一节点接上(就是新的根节点和旧的根节点互换位置)

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
// 获取旧的根节点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
// 数组桶指向新的根节点
tab[index] = root; // 断开 root 在原链表中的连接:先取出root上一节点
TreeNode<K,V> rp = root.prev;
// 再将前后节点连接起来,从而断开root 在原链表中的连接
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn; // 新的根节点调整为头节点
if (first != null)
first.prev = root;
// 旧的根节点成为头节点的下一节点
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}

总的来说,没什么深奥的,就是单链表和双链表的作用区别,为了任意树节点都可以更快的找到上一节点,提高操作效率。

4. 总结

HashMap插入流程、扩容流程、查询流程,以及删除节点时链表和红黑树的处理。对 HashMap 会有一个基本而完整的理解。接下来可以深入学习红黑树数据结构,这是学习HashMap、LinkedHashMap、TreeMap等集合必须掌握的数据结构。

Java集合--HashMap底层原理可视化,秒懂扩容、链化、树化

Java集合--从本质出发理解HashMap

Java集合--LinkedList源码可视化

Java集合源码--ArrayList的可视化操作过程

掌握设计模式的两个秘籍

查看往期设计模式文章的:设计模式

超实用的SpringAOP实战之日志记录

2023年下半年软考考试重磅消息

通过软考后却领取不到实体证书?

计算机算法设计与分析(第5版)

Java全栈学习路线、学习资源和面试题一条龙

软考证书=职称证书?

软考中级--软件设计师毫无保留的备考分享

原创不易,觉得还不错的,三连支持:点赞、分享、推荐↓

HashMap集合--基本操作流程的源码可视化的更多相关文章

  1. List-LinkedList、set集合基础增强底层源码分析

    List-LinkedList 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 继上一章继续讲解,上章内容: List-ArreyLlist集合基础增强底层源码分析:https:// ...

  2. List-ArrayList集合基础增强底层源码分析

    List集合基础增强底层源码分析 作者:Stanley 罗昊 [转载请注明出处和署名,谢谢!] 集合分为三个系列,分别为:List.set.map List系列 特点:元素有序可重复 有序指的是元素的 ...

  3. 【转】HashMap,ArrayMap,SparseArray源码分析及性能对比

    HashMap,ArrayMap,SparseArray源码分析及性能对比 jjlanbupt 关注 2016.06.03 20:19* 字数 2165 阅读 7967评论 13喜欢 43 Array ...

  4. java集合系列之LinkedList源码分析

    java集合系列之LinkedList源码分析 LinkedList数据结构简介 LinkedList底层是通过双端双向链表实现的,其基本数据结构如下,每一个节点类为Node对象,每个Node节点包含 ...

  5. java集合系列之ArrayList源码分析

    java集合系列之ArrayList源码分析(基于jdk1.8) ArrayList简介 ArrayList时List接口的一个非常重要的实现子类,它的底层是通过动态数组实现的,因此它具备查询速度快, ...

  6. 集合之HashMap(含JDK1.8源码分析)

    一.前言 之前的List,讲了ArrayList.LinkedList,反映的是两种思想: (1)ArrayList以数组形式实现,顺序插入.查找快,插入.删除较慢 (2)LinkedList以链表形 ...

  7. [源码解析]HashMap和HashTable的区别(源码分析解读)

    前言: 又是一个大好的周末, 可惜今天起来有点晚, 扒开HashMap和HashTable, 看看他们到底有什么区别吧. 先来一段比较拗口的定义: Hashtable 的实例有两个参数影响其性能:初始 ...

  8. Java集合系列[4]----LinkedHashMap源码分析

    这篇文章我们开始分析LinkedHashMap的源码,LinkedHashMap继承了HashMap,也就是说LinkedHashMap是在HashMap的基础上扩展而来的,因此在看LinkedHas ...

  9. HashMap就是这么简单【源码剖析】

    前言 声明,本文用得是jdk1.8 前面已经讲了Collection的总览和剖析List集合以及散列表.Map集合.红黑树的基础了: Collection总览 List集合就这么简单[源码剖析] Ma ...

  10. HashMap 与 ConcrrentHashMap 使用以及源码原理分析

    前奏一:HashMap面试中常见问题汇总 HashMap的工作原理是近年来常见的Java面试题,几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道HashTable和Has ...

随机推荐

  1. 用户代码未处理 SqlException

    场景重现 客户端连接 Sql Server 2008 R2 数据库出现如下错误: 错误原因 后发现是数据库服务是手动启动的,服务器更新重启后,SQL Server服务没自动启动... 解决办法 把SQ ...

  2. 活动中台系统慢 SQL 治理实践

    作者:vivo 互联网服务器团队- Zhang Mengtao 活动中台系统作为中台项目非常注重系统性能和用户体验,数据库系统性能问题会对应用程序的性能和用户体验产生负面影响.慢查询可能导致应用程序响 ...

  3. Python3_python2打包exe文件

    最近要把绿盟报告导出脚本打包成一个exe,原本是一个py2的文件Vulreport.py,我做了如下步骤. 1.py2topy3 Python3 2to3.py -w Vulreport.py 2.p ...

  4. Oracle 使用UTL_HTTP发送http请求--转载

    参考:https://blog.csdn.net/tmaczt/article/details/82665885 GET方式 CREATE OR REPLACE FUNCTION FN_HTTP_GE ...

  5. 【工具】Typora中主题css修改|看了这篇,一劳永逸

    真正的指南 1. 查看当前的css shift+f12,与一般浏览器调试一样,先打开控制台,查找你需要修改的地方叫什么名字.(也可以点击"视图"-"开发者工具" ...

  6. MongoDB从入门到实战之Windows快速安装MongoDB

    前言 本章节的主要内容是在 Windows 系统下快速安装 MongoDB 并使用 Navicat 工具快速连接. MongoDB从入门到实战之MongoDB简介 MongoDB从入门到实战之Mong ...

  7. LangChain4j如何自定义文档转换器实现数据清洗?

    LangChain4j 提供了 3 种 RAG(Retrieval-Augmented Generation,检索增强生成)实现,我们通常在原生或高级的 RAG 实现中,要对数据进行清洗,也就是将外接 ...

  8. Django REST框架中处理JWT令牌的认证的源码解析

    想了解`JWTAuthentication`这个类的源码解析.`JWTAuthentication`是来自`rest_framework_simplejwt.authentication`模块的,它用 ...

  9. 网络编程:非阻塞I/O

    阻塞VS非阻塞 阻塞I/O:应用程序会被挂起,等待内核完成操作,实际上,内核所做的事情是将CPU时间切换给其他有需要的进程,网络应用程序在这种情况下是得不到CPU时间做该做的事情的. 非阻塞I/O:当 ...

  10. c语言笔记(翁凯男神

    哼,要记得好好学习去泡帅哥吖 一.快速入门 %p 输出地址 #include <stdio.h> void f(int *p); int main(){ int i = 1; printf ...