Put函数源码解析HashMap的put方法执行过程可以通过下图来理解(摘自某大厂的博客,推荐从参考文献的链接去查看原文),自己有兴趣可以去对比源码更清楚地研究学习。

  欲了解更多HashMap,请戳《HashMap知识点梳理、常见面试题和源码分析》。

HashMap Put函数源码流图解

① 判断哈希数组table是否为空或为null,若是则执行resize()进行扩容;

② 根据键值key的hashCode值求得数组索引i,如果table[i]==null,直接添加元素,转向⑥;否则,转向③;

③ 判断table[i]的首个元素是否和key一样,如果相同则覆盖旧的value;否则,转向④,这里的相同指的是hashCode以及equals;

④ 判断table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对;否则,转向⑤;

⑤ 判断table[i]的链表长度是否不小于8,不小于8而且数组长度不小于常量64时(图中标记的不准确),把链表转换为红黑树,在红黑树中执行插入操作;否则,在链表尾部插入;遍历过程中若发现key已经存在,则直接覆盖value;

⑥ 插入成功后,判断HashMap中实际存在的键值对数量是否超过最大容量,如果超过,则调用扩容函数resize进行扩容。

  至此,大家对put函数执行流程有了一个大致认识,下面跟着楼兰胡杨看源码吧!Java 8 中,HashMap的实现相比于Java 7做了很大的改变,主要包括以下几点:

  但是,从Java 8 到 Java 13 ,其实现并未改变,下面以 Java 13中 HashMap Put函数源码蓝本,进行分析:

     /**
* The number of key-value mappings contained in this map.
* map中键值对个数
*/
transient int size; /**
* 链表转为红黑树的第一个条件:链表元素临界值
*/
static final int TREEIFY_THRESHOLD = 8;
/**
  * 链表转红黑树的第二个条件:对数组长度的最低要求。bins就是数组中的各个桶
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64; /**
* 红黑树退化为链表的临界值
*/
static final int UNTREEIFY_THRESHOLD = 6;
// 构造函数 put
public V put(K key, V value) {
// 对key的hashCode()做hash
return putVal(hash(key), key, value, false, true);
} final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//定义一个数组tab,一个Node节点p,n用于存放数组长度,i用于存放key在数组中的索引
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 步骤①:判断table是否为空或数组长度为0,如果为空则通过resize()实例化一个数组并让tab作为其引用,并且让n等于实例化tab后的长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤②:计算数组下标i,并对null做处理
// 如果首结点值为空,则创建一个新的首结点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 出现哈希碰撞,开始处理,重新定义一个Node,和一个k
Node<K,V> e; K k; // 步骤③:节点key存在,则覆盖旧值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 判断元素相等的标准:p与新元素有相同的哈希值和key值
// 步骤④:判断该链为红黑树
// 如果首结点的类型是红黑树类型,则按照红黑树方法添加该元素
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 步骤⑤:判断该链为链表
else {
for (int binCount = 0; ; ++binCount) {
// 遍历到链表末尾时再追加该元素,表尾的next节点必然为null
if ((e = p.next) == null) {
//直接将数据写到下个节点
p.next = newNode(hash, key, value, null);
// 当桶中元素个数不小于8且数组长度不小于64时,把单链表转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
// 链表转红黑树,源码见下一节「桶的树形化」,注意:该方法不一定会将该位置的Node转换成红黑树
treeifyBin(tab, hash);
break;
}
// 如果找到与新元素具有相同的hash和key值的结点,则停止遍历。此时e已经记录了该结点,**直接跳出,不做任何操作**
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;// p指向下一个节点
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 如果允许修改节点,则修改
if (!onlyIfAbsent || oldValue == null)
// 用新value覆盖旧值
e.value = value;
afterNodeAccess(e); //节点被访问的回调函数,可根据需要覆盖
return oldValue; // 返回旧值
}
}
++modCount; //如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null
//步骤⑥:HashMap中键值对个数超过最大容量时进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 这个是空函数,可根据需要覆盖
return null;
}

  分析:

1 数组tab为null或为空?若是,则resize();

2 计算key的hash值i,若tab[i]==null,新建节点;否则,转入3

3 判断首节点处理hash冲突的方式是链表还是红黑树(check第一个节点类型),分别处理。

例如,链表尾插法——使用链表处理时,利用Node类的next变量来实现链表,把最新的元素放到链表尾部,旧的数据则被最新元素的next变量引用。

确定哈希数组索引位置

  无论是增加和删除操作,还是查找键值对,定位到桶在数组中的位置都是关键的一步。前面说过HashMap的数据结构是数组和单链表的结合,所以当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个桶里只有一个元素。故当用hash算法求得这个桶的位置时,马上就可以知道此位置的元素就是我们需要的,避免遍历链表,大大优化了查询效率。HashMap定位数组索引位置的函数直接决定了hash方法的离散性能。先看看源码的实现(方法一+方法二):

方法一:
static final int hash(Object key) { //java 8 和 java 7
int h;
// h = key.hashCode() 为第一步 调用hashCode()
// h ^ (h >>> 16) 为第二步 高16位异或低16位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
//java 7的源码,java 8没有这个方法,但是实现原理一样。length是数组长度。
static int indexFor(int h, int length) {
return h & (length-1); //第三步 对数组长度取模运算,计算数组下标
}

  这里的Hash算法本质上可以拆分为三步:取key的hashCode值、高位异或运算、对数组长度取模运算。

  对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用方法二来计算该对象应该保存在table数组的哪个索引处。

  这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,与运算 h& (length-1) 等价于对数组长度length取模,也就是h%length,但是&比%具有更高的效率。

  在java 8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低位都参与到数组下标的计算中,同时不会有太大的开销。

桶的树形化

  链表转红黑树的函数 treeifyBin() 并非是直接把链表转成红黑树,而是先判断是扩容还是转成红黑树。源码如下:

//将tab[index] 桶内所有的链表节点转换成红黑树节点
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果当前哈希表为空,或者数组长度小于64,则扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//数组长度不小于64,进行树形化
// e 是哈希表中指定桶里的链表节点,从第一个开始
TreeNode<K,V> hd = null, tl = null; //红黑树的头、尾节点
do {
//以当前链表节点 e 新建一个树形节点
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);
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}

结束语

  深入探讨HashMap的结构实现和功能原理。

Reference

HashMap put方法源码解析|Java 17的更多相关文章

  1. Java 集合系列 09 HashMap详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  2. Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例

    概要 这一章,我们对HashMap进行学习.我们先对HashMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用HashMap.内容包括:第1部分 HashMap介绍第2部分 HashMa ...

  3. erlang下lists模块sort(排序)方法源码解析(一)

    排序算法一直是各种语言最简单也是最复杂的算法,例如十大经典排序算法(动图演示)里面讲的那样 第一次看lists的sort方法的时候,蒙了,几百行的代码,我心想要这么复杂么(因为C语言的冒泡排序我记得不 ...

  4. erlang下lists模块sort(排序)方法源码解析(二)

    上接erlang下lists模块sort(排序)方法源码解析(一),到目前为止,list列表已经被分割成N个列表,而且每个列表的元素是有序的(从大到小) 下面我们重点来看看mergel和rmergel ...

  5. 【转】Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例

    概要 这一章,我们对HashMap进行学习.我们先对HashMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用HashMap.内容包括:第1部分 HashMap介绍第2部分 HashMa ...

  6. TreeSet集合的add()方法源码解析(01.Integer自然排序)

    >TreeSet集合使用实例 >TreeSet集合的红黑树 存储与取出(图) >TreeSet的add()方法源码     TreeSet集合使用实例 package cn.itca ...

  7. 【Java实战】源码解析Java SPI(Service Provider Interface )机制原理

    一.背景知识 在阅读开源框架源码时,发现许多框架都支持SPI(Service Provider Interface ),前面有篇文章JDBC对Driver的加载时应用了SPI,参考[Hibernate ...

  8. 解析jQuery中extend方法--源码解析以及递归的过程《二》

    源码解析 在解析代码之前,首先要了解extend函数要解决什么问题,以及传入不同的参数,会达到怎样的效果.extend函数内部处理传入的不同参数,返回处理后的对象. extend函数用来扩展对象,增加 ...

  9. Java基础知识强化63:Arrays工具类之方法源码解析

    1. Arrays工具类的sort方法: public static void sort(int[] a): 底层是快速排序,知道就可以了,用空看. 2. Arrays工具类的toString方法底层 ...

  10. jvm源码解析java对象头

    认真学习过java的同学应该都知道,java对象由三个部分组成:对象头,实例数据,对齐填充,这三大部分扛起了java的大旗对象,实例数据其实就是我们对象中的数据,对齐填充是由于为了规则分配内存空间,j ...

随机推荐

  1. SuperSocket 服务端 和 SuperSocket.ClientEngine 客户端及普通客户端

    internal class Program { //static void Main(string[] args) //{ // byte[] arr = new byte[1024]; // 1. ...

  2. 复杂任务分解:Tree of Thought

    像搭乐高一样玩转AI思考 今天要带大家解锁一个让AI从"单细胞生物"进化成"八爪鱼思考者"的神技--Tree of Thought(思维树).准备好了吗?我们要 ...

  3. 文件上传fuzz工具-Upload_Auto_Fuzz

    一.工具介绍 ​ 在日常遇到文件上传时,如果一个个去测,会消耗很多时间,如果利用工具去跑的话就会节省很多时间,本Burp Suite插件专为文件上传漏洞检测设计,提供自动化Fuzz测试,共300+条p ...

  4. 国产数据库高光时刻!天翼云TeleDB荣登TPC-DS全球测评总榜第二

    近日,天翼云TeleDB数据库以40206063QphDS的吞吐量在国际权威机构TPC(国际事务处理性能委员会)发布的数据库基准测试TPC-DS中荣登全球榜单第二位.中国数据库技术跻身国际顶尖行列,这 ...

  5. 【Bug记录】[@vue/compiler-sfc] `defineProps` is a compiler macro and no longer needs to be imported.

    [Bug记录][@vue/compiler-sfc] defineProps is a compiler macro and no longer needs to be imported. Vue3项 ...

  6. Django实战项目-学习任务系统-任务完成率统计

    接着上期代码内容,继续完善优化系统功能. 本次增加任务完成率统计功能,为更好的了解哪些任务完成率高,哪些任务完成率低. 该功能完成后,学习任务系统1.0版本就基本完成了. 1,编辑urls配置文件:. ...

  7. 3.14 + 1e10 - 1e10 = 0 ? ——浮点数的本质

    3.14 + 1e10 - 1e10 = 0 ? --浮点数的本质 我们先看这样一个例子: #include <iostream> int main(int argc, char **ar ...

  8. Java24发布,精心总结

    Java 24作为2025年3月发布的最新版本,延续了Java平台每半年发布一次的节奏,带来了24项重要改进.本文将按照核心改进领域分类,详细解析每个特性的技术原理和实际价值,帮助开发者全面了解这一版 ...

  9. Oracle SQL%ROWCOUNT

    SQL%ROWCOUNT 用于记录受影响的行数, 必须紧跟在一个新增/修改/删除类语句后. 当执行多条修改语句时, 按照 sql%rowcount 之前执行的最后一条语句受影响行数为准. 应用场景 可 ...

  10. study Rust-5【Slice】

    另一个没有所有权的数据类型是 slice.slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合. [字符串Slice熟悉掌握的很勉强,通过动手来进步加深认识] 字符串slice let ...