解读HashMap中put方法的源码

2023-10-30

解析hashMap的put方法是如何存储一个键值。

一、 put方法

代码1-1 V put(K key, V value)方法

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

调用put方法存储键值,实际上是调用了HashMap的putVal方法,putVal方法的调用过程中 执行了hash(key) 获取了一个int类型的值

代码1-2 int hash(Object key)方法

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

首先该hash方法声明了一个 int类型的值 h ,int类型占4个字节 共32位,然后调用key的hashCode方法获取一个key的散列码 并将该散列码赋值给h,最后执行h^(h>>>16)并返回结果。

解析h^(h>>>16),首先将h无符号右移16位,右移的结果是 32位h的低16位变为高16位的值,而高16位则全部变为0 例如:

0010 1100 1010 0000 0001 0100 1010 0001  将h无符号右移16位--》

0000 0000 0000 0000 0010 1100 1010 0000  位移后的结果

之后将h 与 位移后的结果做一个异或运算,整个操作也可以解读为 h的高16位保持不变,将高16位和低16位异或的结果赋值给 低16位 ,最终返回一个更加散列的散列码(也就是将key.hashCode()再一次散列)。

二、 putVal方法

我编写了一段代码 通过debug来观察putVal方法的执行过程,如下:

代码2-1 自定义代码(debug的断点位置已标明)

    public static void main(String[] args) {
        HashMap<strKey, Object> stringObjectHashMap = new HashMap<>();
        strKey strKey = new strKey("a", "b");
        strKey strKey2 = new strKey("b", "a");
        //strKey和strKey2的哈希值相同
        System.out.println(strKey.hashCode());
        System.out.println(strKey2.hashCode());
 断点1   stringObjectHashMap.put(strKey, "hello");
 断点2   stringObjectHashMap.put(strKey2, "hi");
    }

    //创建全参构造方法,重写equals和hashcode方法
    @AllArgsConstructor
    public static class strKey {
        private String a;
        private String b;

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            strKey strKey = (strKey) o;
            return Objects.equals(a, strKey.a) && Objects.equals(b, strKey.b);
        }

        @Override
        public int hashCode() {
            if (a == null || b == null)
                return 0;
            int result = 1;
            result = 31 * result + a.hashCode() + b.hashCode();
            return result;
        }
    }

putVal方法的代码如下

代码2-2 V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //table是一个 Node<K,V> 类型的数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            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 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

debug 的执行过程如下:

在构造出HashMap类型的对象后,执行断点1处的put方法第一次存储一个键值。

HashMap中存在一个成员变量table变量table是 Node<K,V> 类型的数组 初始为null,putVal方法最先执行如下代码 第一次调用resize方法做容量的初始化。

代码2-3 putVal方法的第一步

//table最开始为null
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

代码2-4  resize()方法第一次被调用时 主要执行的代码

//声明newCap、newThr ,newCap 为新的容量,newThr为最新的扩容阈值
int newCap, newThr = 0

/**
DEFAULT_INITIAL_CAPACITY的值为 1 << 4 ,即值为16 也就是默认的初始化容量是16,DEFAULT_LOAD_FACTOR的值为0.75f , 也就是 默认的加载因子为0.75
**/
newCap = DEFAULT_INITIAL_CAPACITY 
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)

//threshold 为扩容阈值 ,是HashMap的成员变量
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;//将新创建的Node<K,V> 数组赋值给成员变量table

return newTab; //返回newTab

 在代码2-3执行结束后,HashMap第一次初始化时的容量 16 已经赋值给 putVal 方法的局部变量 n

,之后执行putVal 方法的第二步:

代码2-5 putVal方法的第二步

第二步的执行过程中 将hash(也就是在代码1-2中将key的hashCode再次散列的结果) 与 (n-1)也就是15的 二进制 进行一个 与运算,运算的结果是数组tab的下标,因为HashMap的结构是 由 数组+链表+红黑树组成 其实就是 Node<K,V>[] table,数组的每个位置 最初都是一个可以延长的链表,所以最开始应该确定 put存放的元素应该存储在 数组table的哪个位置。

(n - 1) & hash 的最终结果 <= 15 ,有兴趣的读者可以去了解一下 & 运算。

/**
在代码2-3中其实就已经将 table 赋值给了 tab,由于是第一次执行,数组的所有位置都为null,所以将 hash、key、value 构造成一个Node<K,V>对象 并赋值给tab[i]。
**/
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

 代码2-6 putVal方法第三步

++modCount; //modCount会加一
//判断数组容量是否超过阈值,如果超过阈值则调用resize()来扩容,现在是第一次添加 不会执行扩容操作。
if (++size > threshold) 
    resize();

至此断点1就执行完毕,下面开始执行断点2

由于断点1的执行过程中已经进行了初始化 那么断点2的执行直接跳过初始化

 代码2-7 putVal方法第一步

首先获取key所在的数组下标。

由于重写了hashCode方法,致使strKey的hashCode和 strKey2的hashCode相同,但是他们equals的结果却不相同,这里使strKey和strKey2的hashCode相同的目的是为了制造散列冲突(散列冲突指的是 ”桶“ 已经被填充的现象,也就是元素对应的node数组下标的位置已经有值了),即由于hashCode相同导致不同的元素存放在node数组的同一位置,要注意hashCode即便是相同的也会出现散列冲突,这里只是为了模拟散列冲突所以设置了相同的hashCode

//发生了散列冲突,if括号内的结果为false,并且将发生冲突的位置的node赋值给 Node<K,V>类型的 p

if ((p = tab[i = (n - 1) & hash]) == null)

由于第一步的结果为false 所以执行如下代码

代码2-8 putVal方法的第二步 

在第一步中已经说明发生散列冲突时,hashCode可能不相同,也就是代码中的 局部变量 hash 可能不相同(前面已经说明过,hash是hashCode再次散列的结果,所以hashCode相同 hash一定相同)

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
else {
    Node<K,V> e; K k;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
    else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    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 1st
                    treeifyBin(tab, hash);
                break;
            }
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;
        }
    }
    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
}

那么我开始解读代码2-8中 else代码块各个步骤的含义:

首先看一下node的结构

每一个node 是由put方法传入的key、value、key所对应的hash以及下一个node组成,其实就是链表的结构,也就是所说的 HashMap是由数组+链表组成,数组中的每一个元素就是 一个node,也就是链表的第一个节点。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
        ...............
}

1、此时p是tab[i]处的元素,也是链表的第一个node

首先判断p的key的hash和要存入的key的hash是否相同,如果hash不相同 也就是 hashCode不相同,那么两个key一定不是同一个对象 结果返回false,但也有可能出现不同对象的hash相同的情况,所以会出现后续判断 (k = p.key) == key || (key != null && key.equals(k)),如果p的key的内存地址和传入的key的内存地址相同则两个key一定相等,或者equals的结果相同,则两个key也可以判定为相同,判断的最终结果为true 则执行 e=p,如下代码块:

if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;
else{ ...................

/**

if为true 则表示p的key和传入的key相同,那么以下操作的意思就是,将put方法的value值覆盖p的value值,也就是所说的 HashMap的put方法中如果key相同,则旧的value将会被新的value替换

**/

if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}

2、 由于strKey和strKey2进行equals的结果为false,那么就执行到下图:

此处会判断p是不是一个树节点,当链表长度大于8 数组长度大于64的时候,链表会转变为红黑树

 由于此时还未转变为红黑树则执行到下一步骤

3、该步骤如图所示:

 要注意的是 for循环中可能执行多次 e=p.next ,p=e的操作 所以binCount可能是链表中的任意一个位置,图中的for循环先执行如下代码:

if ((e = p.next) == null) {//if判断p的下一个node是否为空
/**
如果p的下一个node为空,则将key、value、以及key对应的hash构造出一个新的node 来作为链表的最后一个node
**/
    p.next = newNode(hash, key, value, null); 
    // TREEIFY_THRESHOLD的值为8,如果链表长度已经达到8 则会调用treeifyBin方法尝试将链表转变为树
    if (binCount >= TREEIFY_THRESHOLD - 1) 
        treeifyBin(tab, hash);
    break;
}

treeifyBin方法的部分代码如下:

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
//MIN_TREEIFY_CAPACITY的值为64,如果数组长度小于64则会调用resize方法进行扩容,否则就会执行将链表转为红黑树的操作
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else{
   ..............
}

for循环也可能执行后面的代码 来跳出循环,如下所示:

这个操作的意思是,可能该链表的某个node的key和传入的key相同,则将新的value覆盖该node的value值

else {
    for (int binCount = 0; ; ++binCount) {
        ........
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            break;
        p = e;
    }
}
if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}

至此结束调试,如有疑问欢迎留言一起探讨。 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

解读HashMap中put方法的源码 的相关文章

  • Java - 为什么不允许 Enum 作为注释成员?

    It says 原始 String Class an Enum 另一个注释 上述任何一个的数组 只有这些类型才是合法的 Annotation 成员 为什么泛型 Enum 不能成为 Annotation 的成员 例如 Retention Re
  • 如何使用 Java 中的 Web 服务(例如 Axis2)发送复杂对象的数组或集合?

    我对 SOAP Web 服务还比较陌生 虽然我完成了一些较小的 Web 服务项目 但我偶然从来不需要返回 或用作参数 复杂 对象的数组或集合 当我尝试这样做时 根据我的 SOAP 绑定风格 我会得到不同的奇怪行为 当我使用RPC 文字 我可
  • 使用 JPA Criteria API 进行分页的总行数

    我正在系统中为实体实现 高级搜索 功能 以便用户可以使用该实体的属性上的多个条件 eq ne gt lt 等 来搜索该实体 我正在使用 JPA 的 Criteria API 动态生成 Criteria 查询 然后使用setFirstResu
  • Java:如何从转义的 URL 获取文件?

    我收到了一个定位本地文件的 URL 事实上我收到的 URL 不在我的控制范围内 URL 按照 RFC2396 中的定义进行有效转义 如何将其转换为 Java File 对象 有趣的是 URL getFile 方法返回一个字符串 而不是文件
  • 如何使用 Java 处理 Selenium WebDriver 中的新窗口?

    这是我的代码 driver findElement By id ImageButton5 click Thread sleep 3000 String winHandleBefore driver getWindowHandle drive
  • Java AES 128 加密方式与 openssl 不同

    我们遇到了一种奇怪的情况 即我们在 Java 中使用的加密方法会向 openssl 生成不同的输出 尽管它们在配置上看起来相同 使用相同的键和 IV 文本 敏捷的棕色狐狸跳过了懒狗 加密为 Base64 字符串 openssl A8cMRI
  • java中如何连接字符串

    这是我的字符串连接代码 StringSecret java public class StringSecret public static void main String args String s new String abc s co
  • 如何安全地解决这个 Java 上下文类加载器问题?

    我的数百名用户中只有一位在启动我的 Java 桌面应用程序时遇到问题 他只有大约三分之一的时间开始 另外三分之二的时间在启动时抛出 NullPointerException Exception in thread AWT EventQueu
  • 我需要什么库才能在 Java 中访问这个 com.sun.image.codec.jpeg?

    我正在用java创建一个图像水印程序 并导入了以下内容 import com sun image codec jpeg JPEGCodec import com sun image codec jpeg JPEGEncodeParam im
  • Hazelcast 分布式锁与 iMap

    我们目前使用 Hazelcast 3 1 5 我有一个简单的分布式锁定机制 应该可以跨多个 JVM 节点提供线程安全性 代码非常简单 private static HazelcastInstance hInst getHazelcastIn
  • hibernate锁等待超时超时;

    我正在使用 Hibernate 尝试模拟对数据库中同一行的 2 个并发更新 编辑 我将 em1 getTransaction commit 移至 em1 flush 之后我没有收到任何 StaleObjectException 两个事务已成
  • 在 Netbeans 8 上配置 JBoss EAP 的问题

    我已经下载了 JBoss EAP 7 并正在 Netbeans 8 上配置它 我已经到达向导 实例属性 其中要求从选择框中选择 域 当我打开选择框时 它是空的 没有什么可以选择的 因此 完成 按钮也处于非活动状态 这使得无法完成配置 我通过
  • Calendar.getInstance(TimeZone.getTimeZone("UTC")) 不返回 UTC 时间

    我对得到的结果真的很困惑Calendar getInstance TimeZone getTimeZone UTC 方法调用 它返回 IST 时间 这是我使用的代码 Calendar cal Two Calendar getInstance
  • Java 中的“Lambdifying”scala 函数

    使用Java和Apache Spark 已用Scala重写 面对旧的API方法 org apache spark rdd JdbcRDD构造函数 其参数为 AbstractFunction1 abstract class AbstractF
  • 欧洲中部时间 14 日 3 月 30 日星期五 00:00:00 至 日/月/年

    我尝试解析格式日期Fri Mar 30 00 00 00 CET 14至 日 月 年 这是我的代码 SimpleDateFormat formatter new SimpleDateFormat dd MM yyyy System out
  • 替换后增量

    我自己已经有一个问题了 但我想扩展它后增量示例 https stackoverflow com questions 51308967 post increment with example char a D int b 5 System o
  • Java中的Object类是什么?

    什么是或什么类型private Object obj Object http download oracle com javase 6 docs api java lang Object html是Java继承层次结构中每个类的最终祖先 从
  • 具有特定参数的 Spring AOP 切入点

    我需要创建一个我觉得很难描述的方面 所以让我指出一下想法 com x y 包 或任何子包 中的任何方法 一个方法参数是接口 javax portlet PortletRequest 的实现 该方法中可能有更多参数 它们可以是任何顺序 我需要
  • 如何从 Maven 存储库引用本机 DLL?

    如果 JAR 附带 Maven 存储库中的本机 DLL 我需要在 pom xml 中放入什么才能将该 DLL 放入打包中 更具体地举个例子Jacob http search maven org artifactdetails 7Cnet s
  • 在 RESTful Web 服务中实现注销

    我正在开发一个需要注销服务的移动应用程序 登录服务是通过数据库验证来完成的 现在我陷入了注销状态 退一步 您没有提供有关如何在应用程序中执行身份验证的详细信息 并且很难猜测您在做什么 但是 需要注意的是 在 REST 应用程序中 不能有会话

随机推荐

  • thread_create 和 thread_resume

    在lk中我们一般通过thread create 来新建一个thread 但这个thread 是THREAD SUSPENDED 必须要调用thread resume 才能开始运行 enum thread state THREAD SUSPE
  • 城市内涝及桥洞隧道积水在线监测系统

    一 方案概述 近几年 全国频频爆发暴雨等极端天气 这对社会管理 城市运行和人民群众生产生活造成了巨大影响 加之部分城市排水防涝等基础设施建设滞后 调蓄雨洪和应急管理能力不足 出现了严重的暴雨内涝灾害 城市中的隧道和立交桥等低洼路段在遭遇短时
  • YOLOv3使用笔记——Kmeans聚类计算anchor boxes

    anchor boxes用来预测bounding box faster rcnn中用128 128 256 256 512 512 分三个尺度变换1 1 1 2 2 1 共计9个anchor来预测框 每个anchor预测2000个框左右 使
  • qedl中的fixDepth()简化

    如果将PerspectiveMode的设置为1 则会传递zNear和Zfar 在fixDepth 中 而将perspectiveMode 0 则大大简化fixDepth float fixDepth float depth return c
  • 贷款预测问题(探索性分析+多种解决方案)

    用到的数据集 train 链接 https pan baidu com s 1hCQKvLYxTb5MkltJDa1QlQ 提取码 jsh8 test 链接 https pan baidu com s 16SkJ7fo1yEutv4CwnW
  • 工厂方法(Factory Method):对象创建型模式

    文章目录 1 代码示例 2 工厂方法模式的定义 实现意图 1 代码示例 工厂方法模式 简称工厂模式或者多态工厂模式 与简单工厂模式相比 引入了更多的新类 灵活性更强 实现也更加复杂 符合开闭原则 付出的代价是需要新增加多个新的工厂类 如下
  • linux 卸载iscsi,iscsi挂载和删除

    iscsi挂载和删除 2011 01 01 11 54 01 分类 存储 字号 iscsi操作 http blog csdn net do2jiang archive 2009 12 29 5097730 aspx trune2fs c l
  • Python学习之:如何根据经纬度来实现地图的可视化(将这些点在地图上标注出来)

    文章目录 最终效果展示 实操步骤 第一步 打开高德地图的控制台 gt 数据可视化 第二步 创建可视化项目 第三步 上传CSV数据 注意格式要求 一定要包含经纬度信息 第四步 创建可视化实例 最终效果展示 这些红色的点 就是我们给出的经纬度的
  • javascript实现回到顶部按钮特效

    有时由于页面内容过多 每次回到顶部比较麻烦 所以记录一下JavaScript实现回到顶部按钮特效的代码 便于以后备用 css代码 实现回到顶部按钮特效 box position fixed right 10px bottom 10px he
  • Hadoop伪分布模式配置

    Hadoop共有三种部署方式 本地模式 伪分布模式及集群模式 本次安装配置以伪分布模式为主 即在一台服务器上运行Hadoop 如果是分布式模式 则首先要配置Master主节点 其次配置Slave从节点 以下说明如无特殊说明 默认使用root
  • QT多窗口

    常用的窗体基类是 QWidget QDialog 和 QMainWindow 在创建 GUI 应用程序时选择窗体基类就是从这 3 个类中选择 QWidget 直接继承于 QObject 是 QDialog 和 QMainWindow 的父类
  • 在学习Python时遇到的一些项目bug

    启动线程 开启任务时 1 出现错误TypeError init got an unexpected keyword argument arg ok python没有能解析出来这个参数 好吧少写了个s 加上就没啥问题了 2 出现错误TypeE
  • 丑数打表 & 计算 (自用)

    丑数定义 只包含因子2 3 5的正整数被称作丑数 define min a b a lt b a b define min4 a b c d min min a b min c d int choushu 5850 int main int
  • 代理模型:最小二乘支持向量回归(LSSVR)--- MATLAB程序

    写在开头 代理模型是工程问题中常用的一个优化方法 当实际问题计算量很大 不容易求解时 可以使用计算量较小 求解迅速的简化模型来替代原模型 加速优化过程 代理模型采用一个数据驱动的 自下而上的办法来建立 首先 通过抽样得到有限个样本点 输入
  • java烟花代码_一个美丽的java烟花程序

    import java awt import java applet import java awt event import javax swing public class ChatApplet extends Applet imple
  • Shell 从入门到精通(二)

    变量的赋值方式 定义或引用变量时注意事项 双引号是弱引用 单引号是强引用 即单引号是所见即所得 双引号是进行了赋值操作 两个反引号等价于 反引号的中的shell命令会被先执行 变量数值运算 1 整数运算 expr 五颗星 2 整数运算 四颗
  • Spring源码解析4.createBean()方法解析

    createBeanInstance protected BeanWrapper createBeanInstance String beanName RootBeanDefinition mbd Nullable Object args
  • ​LeetCode刷题实战336:回文对

    算法的重要性 我就不多说了吧 想去大厂 就必须要经过基础知识和业务逻辑面试 算法面试 所以 为了提高大家的算法能力 这个公众号后续每天带大家做一道算法题 题目就从LeetCode上面选 今天和大家聊的问题叫做 回文对 我们先来看题面 htt
  • 山石岩读丨前沿领域探析——汽车CAN总线协议详解及攻击面分析

    1 CAN总线的基本概念以及由来 CAN Controller Area Network 总线协议是由 BOSCH 发明的一种基于消息广播模式的串行通信总线 它起初用于实现汽车内ECU之间可靠的通信 后因其简单实用可靠等特点 而广泛应用于工
  • 解读HashMap中put方法的源码

    解析hashMap的put方法是如何存储一个键值 一 put方法 代码1 1 V put K key V value 方法 public V put K key V value return putVal hash key key valu