HashMap源码详解

devtools/2024/11/24 22:01:00/

提醒你一下,用电脑或者平板看,手机屏幕小,图片会看不清楚,源码不方便看

基础前置

看该篇文章之前先看看这篇基础文章       HashMap底层原理-CSDN博客 

 先来看HashMap里面的一些参数。

1.DEFAULT_INITIAL_CAPACITY 默认的数组初始容量

2. DEFAULT_LOAD_FACTOR 默认的加载因子。(hashmap数组扩容机制:  扩容阈值 = 数组容量 * 加载因子)

3. table 你知道的,hashmap底层是数组+链表+红黑树,不知道就先看上面说的基础文章,table就是这里面的数组。

4. size 集合中元素个数。

 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16static final float DEFAULT_LOAD_FACTOR = 0.75f;transient Node<K,V>[] table;transient int size;

 这个是上面第三行代码里面的Node简化后的代码,源码还重写了equals和hashcode方法等,先不关注这些,看看简化后的就行。

64fc9b7940d64e3fa7e07632aaa9a513.png

构造方法

    idea中ctrl+鼠标左键点进去看看源码。

 404fbdcedee4448ea9d424edfe47ea19.png

查看下面源码可知:

1. HashMap是懒加载,也就是说,Hashmap在初始化的时候没有创建数组,只是初始化了加载因子

2. 无参构造函数里面,加载因子初始化为0.75

  public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}

put方法源码

 直接看代码可能不够直观,我们结合流程图来看,仔细耐心看完,顺着把每个流程走一下,待会儿看源码就会清楚很多。 实在看不懂的话,看源码的时候再来结合图片看。

        下图threshold就是上面提到的扩容阈值,扩容阈值 = 数组容量 * 加载因子。

        而且你可能会迷惑为什么右下角转化为红黑树只需要判断链表长度,不应该还需要判断数组长度吗? 这个你往下看,我会解答的。

       并且,转化条件是链表长度>=8且数组长度>=64,下图是我网上找的,有点小错误,这里修正一下。133221d11c4c41f4a7ca7e49dc39b0af.png

HashMap里面的put方法

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

第一个参数hash(key),其实就是计算key的哈希值,对应数组哪个下标位置。

   static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

现在看看上述的putVal方法。注释写了很久,各位爷给个三连,不懂的留言评论区,24小时内回复。

上述图片代码贴在下面了

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//判断数组是否初始化,我们上面介绍过hashmap是懒加载,创建hashmap不会初始化,第一次插入值才初始化。    if ((tab = table) == null || (n = tab.length) == 0)//如果没有初始化,调用resize()进行初始化,第一次初始化长度为16。resize方法源码后面也会带着大家看,现在先不着急。n = (tab = resize()).length;//(n - 1) & hash是与运算,算出了当前key、value插入到数组对应位置下标。if ((p = tab[i = (n - 1) & hash]) == null)//如果该下标为null,那么直接插入就行,进而转到58行代码tab[i] = newNode(hash, key, value, null);//如果不为null,就要进一步判断else {Node<K,V> e; K k;//判断该位置key和新传来的key是否一样if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//如果一样,证明为修改操作,把该值赋值给p,直接转到48行代码e = p;//判断是不是红黑树else if (p instanceof TreeNode)//如果是红黑树,走红黑树的添加逻辑e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//既不是红黑树,key也不相同,证明是链表。else {//遍历链表for (int binCount = 0; ; ++binCount) {//找到next节点,如果为null,证明遍历到尾部了if ((e = p.next) == null) {//把新值放入链表尾部p.next = newNode(hash, key, value, null);//链表新插入节点,有转换成红黑树的可能,要判断链表长度是否大于等于8if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//如果是,就转化成红黑树,这个转化操作就不带大家看了,感兴趣自己研究一下。treeifyBin(tab, hash);break;}//判断是否有key相同的值,如果有那就是修改操作,break后转到48行代码继续执行if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//e是修改操作中存放原数据的变量if (e != null) { // existing mapping for key//新值覆盖旧值V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}//计数器,计算当前节点修改次数++modCount;//上面插入成功后执行++size,然后判断数据量是否大于阈值,大于的话数组就进行resize()扩容   if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

        看到这,你应该大致明白了底层逻辑,但是有个坑我故意留给大家了,35行代码为什么只需要判断链表长度大于8就转成红黑树呢?  不是说,要链表长度>=8且数组长度>=64吗。如果你对hashmap代码真的非常感兴趣,应该去看了第37行代码,方法内部的源码,treeifyBin(tab, hash);

        现在我来带大家简单看看

        下面的MIN_TREEIFY_CAPACITY=64,当数组长度小于64的时候执行扩容操作,否则才执行链表转化红黑树操作。至此,put的基本流程已经结束。

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);}}

  

现在接着讲讲扩容机制吧

扩容机制

        这个就看看流程图吧,我详细给大家讲讲流程图。源码我后续补上,脑袋转不动了。

        橙色部分是第一次插入数据扩容,数组还没有初始化的时候。

        蓝色部分是第二次及以后的扩容了,直接扩2倍新建数组,然后遍历旧数组把值加入到新数组,这里又分为三种情况。

1.  只有一个节点,取出当前节点哈希值,与新数组长度进行与运算找到在新数组存储的下标位置进行存储

2.  如果是红黑树,直接走红黑树添加逻辑。

3.  如果是链表,遍历链表,进行当前值的hash和旧容量的与运算,也就是e.hash & oldCap,如果该值等于0,该值在新数组的下标和就数组一样,如果该值不等于0,那么放入新数组下标为j+oldCap(旧数组下标+旧容量)的地方,这样看来,第三步链表的转移操作是有可能把链表拆分的。d6ccb25263614cb6b5c9f1b468685d18.png

看完后你可以来看看这位大佬的hashmap提问,看你能答上来几道,里面有答案。

HashMap夺命14问,你能坚持到第几问?-腾讯云开发者社区-腾讯云


http://www.ppmy.cn/devtools/136651.html

相关文章

Pytorch使用手册-Automatic Differentiation with torch.autograd(专题六)

自动微分(Automatic Differentiation)和 torch.autograd torch.autograd 是 PyTorch 中的核心模块之一,用于支持自动微分。它通过动态计算图的方式实现梯度计算,主要应用于深度学习中的反向传播算法,自动求导无需手动计算梯度,简化了训练神经网络的过程。以下为模块核心…

Origin教程003:数据导入(2)-从文件导入和导入矩阵数据

文章目录 3.3 从文件导入3.3.1 导入txt文件3.3.2 导入excel文件3.3.3 合并工作表3.4 导入矩阵数据3.3 从文件导入 所需数据 https://download.csdn.net/download/WwLK123/900267473.3.1 导入txt文件 选择【数据->从文件导入->导入向导】: 选择文件之后,点击完成即可…

ElasticSearch学习笔记四:基础操作(二)

一、前言 上一篇文章中我们学习了ES中的基础操作&#xff0c;包括索引和映射&#xff0c;同时也学习了ES中的基础数据类型&#xff0c;今天我们继续学习其他的数据类型。 二、复杂数据类型 1、数组&#xff08;Array&#xff09; 在ES中没有特别指定数据类型&#xff0c;换…

react和vue图片懒加载及实现原理

一、实现原理 核心思想&#xff1a; 只有当图片出现在视口中时&#xff0c;才加载图片。利用占位图或占位背景优化用户体验。 实现技术&#xff1a; 监听滚动事件&#xff1a;监听页面滚动&#xff0c;通过计算图片与视口的位置关系&#xff0c;判断是否需要加载图片。Intersec…

在 Ubuntu 系统上安装 npm 环境以及 nvm(Node Version Manager)

在 Ubuntu 系统上安装 npm 环境以及 nvm&#xff08;Node Version Manager&#xff09; 步骤 1: 更新系统包步骤 2: 安装 nvm步骤 3: 安装 Node.js 和 npm步骤 4: 设置默认 Node.js 版本&#xff08;可选&#xff09;总结 在 Ubuntu 系统上安装 npm 环境以及 nvm&#xff08;No…

GitLab|数据迁移

注意&#xff1a;新服务器GitLab版本需和旧版本一致 在旧服务器执行命令进行数据备份 gitlab-rake gitlab:backup:create 备份数据存储在 /var/opt/gitlab/backups/ 将备份数据传输到新服务器的/var/opt/gitlab/backups/下&#xff0c;并修改文件权限&#xff08;下载前和上传…

Gate学习(6) 指令学习3

一、/particle/ 目录及其子目录下的命令 在 `/particle/` 命令目录及其子目录下,可以控制和管理粒子相关的属性和过程。以下是每个命令目录和命令的简要解释: ### `/particle/` 这是粒子控制命令的主目录,包括选择粒子、列出粒子名称、查找粒子编码、创建所有离子和同位旋等…

调大Vscode资源管理器字体

对于调整资源管理器字体大小&#xff08;也就是下图红框&#xff09;&#xff0c;查找了网上很多方法。要么介绍的方法是调整了代码字体&#xff0c;要么是调节了终端字体&#xff0c;要么是通过整体放缩实现的调整&#xff0c;总之都不合适。 唯一的调整方法是在几篇CSDN里看到…