HashMap源码-Put详解(HashMap是如何添加元素的)

2023-11-07

HashMap是Java中很重要一个部分,内容较多,因此笔者在此将其拆成一个个小块,作为自己学习知识整理的同时,也和广大网友一起讨论。

也因此,在完成系列的学习之前,将以这种小节的形式进行学习分享,并在学习结束后进行整合,排序。

一、HashMap的实际结构

首先,我们必须了解一下HashMap的实际结构:

bc1237a79ba21a22aa966c87a011a572.png

(图片来自:'是一篇很好的博客,如有时间,希望大家也能花时间在这篇博客中学习一二)https://blog.csdn.net/weixin_39621427/article/details/112096553 如上图所示,HashMap本质是一个数组,其中数组中的每个元素可以指向一套红黑树/一个链表(一定是两种之一)。

在HashMap初始化中,存在一个参数:

树化阈值(TREEIFY_THRESHOLD):

        当数组的节点数超过树化阈(yu,四声)值后 ,会被转化为红黑树的储存结构,可以加快遍历速度。

        在源码中,树化阈值被默认设定为8,如下所示:

pubic static final int TREEIFY_THRESHOLD = 8

 二、HashMap元素的添加

在正常的使用中,我们会这样进行HashMap的初始化和元素的添加:

        Map<String,Integer> map = new HashMap<String, Integer>();
        map.put("a",1);

其中,.put方法就是HashMap中添加元素的方法,那么让我们看看其源码是如何实现的吧。

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

在源码中,我们很容易发现,put方法的实现都在putVal方法中,因此,我们对put方法的重点就在putVal方法中。

putVal方法

查询源码,我们可以看到putVal的源码如下,乍看起来内容较多,在笔者的学习过程中,网络上大部分的资料均是直接开始介绍整套代码的作用,对于初学者来说,需要认知的事情太多,学习起来困难,复习起来更是无从下手,因此,笔者在这里就以初学者的角度开始,转换一下学习的方法。

首先,大致看上去,putVal中包含多个if-else结构,因此,如果我们能将每个if的条件判断弄清楚,那么整个putVal的运行逻辑就一清二楚啦。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        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;
    }

1、方法的声明

从方法的头部往下看,首先,我们需要关注的是方法的声明。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict)

如图,我们发现putVal是存在返回值类型,有5个输入参数的函数,那么我们首先就需要对其中的5个输入参数进行分析。

我们知道,自己在调用put的时候,往往只提供了两个输入参数key与value,那么剩下的参数put是如何传递下去的呢?

在这里,我们就要看看put的源码:

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

如上,put是分别将:

  1. key的哈希值
  2. key
  3. value
  4. false
  5. true 

传递下去的,因此,我们必须清楚除了key与value之外的其他三个值代表的意思。

1)参数1:hash(key)

        在这里,我们发现这里调用了方法hash,详细内容笔者将会在其他博客中详细讲解,在这里,我们只需要知道hash是一种数据处理方法,而hash(key)就是将key通过处理之后得到的一个数即可。

        同时,我们还需要记住,hash是一种处理方法,那么就会有以下的情况,也因此,我们在后面需要针对处理:

  1. 相同的key,经过处理后得到的hash(key)相同
  2. 不同的key,通过处理后得到的hash(key)可能相同

2)参数4:boolean onlyIfAbsent

        表示只有当对应位置为空的时候替换元素。默认为false,即当对应位置不为空的时候,会将旧的value替换成新输入的value。

        在jdk1.8中,新增了方法public V putIfAbsent(K key, V value)可以更改该参数。 

3)参数5:boolean evict

        默认为true,仅在初始化的时候,会传递为false。

2、If-else结构

0)序:

Node<K,V>[] tab; Node<K,V> p; int n, i;

        我们发现,在putVal初始,便定义了诸多参数,虽然其初始化内容和赋值在文后,但我们在最开始对其的作用进行了解后,更能方便我们后面对其进行理解。

  1. Node<K,V>[] tab :        对应整个HashMap数组
  2. Node<K,V> p      :        对应数组的下标(数组尾的下标)
  3. int n                     :        对应HashMap数组的长度
  4. int i                      :         对应数组的遍历参数

1)第一个If

        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

 在第一个if中,tab被赋值为HashMap数组。

        其中,判断tab是否为空(HashMap数组是否为空)

        或者 数组长度是否为零

        如果是,则进行数组的初始化(通过调用resize()方法实现),默认长度为16。(会在其余博客中讲解)。

2)第二个if(无hash冲突)

        在第二个if中,出现了if-else结构,是整个方法中占比最大的部分。

        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        如果当前的数组尾部为空(==null),则将当前输入的参数存入。

3)第二个if对应的else(hash冲突)

        在该代码块中,出现了两个新的参数,同样,也在此进行介绍,以方便后文的理解。

Node<K,V> e; K k;
  1. Node<K,V> e :用于记录该节点中是否存在与新元素key相同的标识,如e为Null则不存在。     (如存在则e不会指向 Null,而是指向key相同的元素的位置。)
  2. K k                  :用于存储key值

        如前文所述,if对应的是数组尾部为空,那么此处对应的就是数组尾部不为空(位置已存在其他元素)。那么这里,如上文中对于hash(key)的介绍,这里会出现两种情况分别对应内容中的第三个if-else结构:

  1. 相同的key导致发生冲突
  2. 不同的key的hash值相同,发生冲突

3-1)第三个if(新元素与节点的key值相同)

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

 如上所示,判断内容为:

        判断:当前数组尾的元素的key和输入参数的key是否相同

        e:指向当前节点的位置

        如判断相同,则用e来储存当前元素的信息,并最终将当前位置元素的Value值用新输入的Value替代。

        替换代码在3-1)的下方,文中稍后讲解。

3-2)else if(节点的key值不相同,且p为treeNode)

        此处判断当前元素p是否为TreeNode,若是,则将红黑树直接插入键值对。

3-3)else(节点的key值不相同,且p不为treeNode)

        for循环->开始遍历该节点,以binCount作为计数器,用于计算当前节点的元素数量。

        注意:此处for循环中,e的数值在一直更新。

3-3-1)for中的第一个if

                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

        遍历:直至节点的末尾(节点元素的.next指向null),如直至末尾仍然无与新元素相同的key值,则将新元素存储在该节点的末尾。

        其中的if:判断节点元素的数量是否超过树化阈值(详见本文第一节内容,默认为8),如超过阈值,则将目前的链表模式转换为红黑树模式。

        e:最终为null。

3-3-2)for中的第二个if

                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;

        看上去十分熟悉是吗,其实是与3-1中的判断语句一致。

        判断:遍历当前节点元素的时候,存在与新元素相同的key,则最终实行Value的更新操作。

        e:最终不为null,指向与新元素的key相同的元素位置。

3-4)第四个if

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

        第四个if是执行语句,如前文中存在与新元素相同的key值,则其对应的Value更新为新元素的Value。

        在执行这种Value的更新的时候,会直接return退出函数,不会对HashMap的大小(参数size)进行增加。

3-5)第五个if-最后的if

if (++size > threshold)
            resize();

        当不为Value值的更新的时候,即为新元素的插入。

        此时,将HashMap的大小增加1,同时

        判断:增加后的size是否超过扩容阈值,如超过,则进行扩容操作(会在其他博客中讲解)。


 至此,结束整个Put的分块式讲解,在这里,笔者将所有的判断整理成框图,相信大家在分块理解后,能很好地理解整个框图的逻辑啦。

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

HashMap源码-Put详解(HashMap是如何添加元素的) 的相关文章

  • 如何让 BlazeDS 忽略属性?

    我有一个 java 类 它有一个带有 getter 和 setter 的字段 以及第二对 getter 和 setter 它们以另一种方式访问 该字段 public class NullAbleId private static final
  • Junit:如何测试从属性文件读取属性的方法

    嗨 我有课ReadProperty其中有一个方法ReadPropertyFile返回类型的Myclass从属性文件读取参数值并返回Myclass目的 我需要帮助来测试ReadPropertyFile方法与JUnit 如果可能的话使用模拟文件
  • 如何通过 javaconfig 使用 SchedulerFactoryBean.schedulerContextAsMap

    我使用 Spring 4 0 并将项目从 xml 移至 java config 除了访问 Service scheduleService 带注释的类来自QuartzJobBean executeInternal 我必须让它工作的 xml 位
  • 为 java 游戏创建交互式 GUI

    大家好 我正在创建一个类似于 java 中的 farmville 的游戏 我只是想知道如何实现用户通常单击以与游戏客户端交互的交互式对象 按钮 我不想使用 swing 库 通用 Windows 看起来像对象 我想为我的按钮导入自定义图像 并
  • 从最终实体获取根证书和中间证书

    作为密码学的菜鸟 我每天都会偶然发现一些简单的事情 今天只是那些日子之一 我想用 bouncy castle 库验证 java 中的 smime 消息 我想我几乎已经弄清楚了 但此时的问题是 PKIXparameters 对象的构建 假设我
  • 检测并缩短字符串中的所有网址

    假设我有一条字符串消息 您应该将 file zip 上传到http google com extremelylonglink zip http google com extremelylonglink zip not https stack
  • 无法创建请求的服务[org.hibernate.engine.jdbc.env.spi.JdbcEnvironment]-MySQL

    我是 Hibernate 的新手 我目前正在使用 Spring boot 框架并尝试通过 hibernate 创建数据库表 我知道以前也问过同样的问题 但我似乎无法根据我的环境找出如何修复错误 休眠配置文件
  • 将 MOXy 设置为 JAXB 提供程序,而在同一包中没有属性文件

    我正在尝试使用 MOXy 作为我的 JAXB 提供程序 以便将内容编组 解组到 XML JSON 中 我创建了 jaxb properties 文件 内容如下 javax xml bind context factory org eclip
  • volatile、final 和synchronized 安全发布的区别

    给定一个带有变量 x 的 A 类 变量 x 在类构造函数中设置 A x 77 我们想将 x 发布到其他线程 考虑以下 3 种变量 x 线程安全 发布的情况 1 x is final 2 x is volatile 3 x 设定为同步块 sy
  • 如何在用户输入数据后重新运行java代码

    嘿 我有一个基本的java 应用程序 显示人们是成年人还是青少年等 我从java开始 在用户输入年龄和字符串后我找不到如何制作它它们被归类为 我希望它重新运行整个过程 以便其他人可以尝试 的节目 我一直在考虑做一个循环 但这对我来说没有用
  • Java ResultSet 如何检查是否有结果

    结果集 http java sun com j2se 1 4 2 docs api java sql ResultSet html没有 hasNext 方法 我想检查 resultSet 是否有任何值 这是正确的方法吗 if resultS
  • 尝试将 Web 服务部署到 TomEE 时出现“找不到...的 appInfo”

    我有一个非常简单的项目 用于培训目的 它是一个 RESTful Web 服务 我使用 js css 和 html 创建了一个客户端 我正在尝试将该服务部署到 TomEE 这是我尝试部署时遇到的错误 我在这里做错了什么 刚刚遇到这个问题 我曾
  • logcat 中 mSecurityInputMethodService 为 null

    我写了一点android应显示智能手机当前位置 最后已知位置 的应用程序 尽管我复制了示例代码 并尝试了其他几种解决方案 但似乎每次都有相同的错误 我的应用程序由一个按钮组成 按下按钮应该log经度和纬度 但仅对数 mSecurityInp
  • 为什么 Java 8 不允许非公共默认方法?

    让我们举个例子 public interface Testerface default public String example return Hello public class Tester implements Testerface
  • 使用 AsyncTask 传递值

    我一直在努力解决这个问题 但我已经到了不知道该怎么办的地步 我想做的是使用一个类下载文件并将其解析为字符串 然后将该字符串发送到另一个类来解析 JSON 内容 所有部件都可以单独工作 并且我已经单独测试了所有部件 我只是不知道如何将值发送到
  • Eclipse 启动时崩溃;退出代码=13

    I am trying to work with Eclipse Helios on my x64 machine Im pretty sure now that this problem could occur with any ecli
  • 我如何在java中读取二进制数据文件

    因此 我正在为学校做一个项目 我需要读取二进制数据文件并使用它来生成角色的统计数据 例如力量和智慧 它的设置是让前 8 位组成一个统计数据 我想知道执行此操作的实际语法是什么 是不是就像读文本文件一样 这样 File file new Fi
  • 在java中为组合框分配键

    我想添加一个JComboBox在 Swing 中这很简单 但我想为组合中的每个项目分配值 我有以下代码 JComboBox jc1 new JComboBox jc1 addItem a jc1 addItem b jc1 addItem
  • 使用 CXF-RS 组件时,为什么我们使用 而不是普通的

    作为后续这个问题 https stackoverflow com questions 20598199 对于如何正确使用CXF RS组件我还是有点困惑 我很困惑为什么我们需要
  • CamcorderProfile.videoCodec 返回错误值

    根据docs https developer android com reference android media CamcorderProfile html 您可以使用CamcorderProfile获取设备默认视频编解码格式 然后将其

随机推荐

  • 第六章 运行时数据结构

    1 a out assembler output 汇编程序输出 的缩写形式 2 段的概念 1 在UNIX中 段表示一个二进制相关的内容块 命令 size test 可执行程序 返回文件中的三个段 text data bss dec hex
  • SQL Server的数据库文件保存在哪儿?

    1 数据库文件类型 数据库分2个文件 一个主数据文件 一个日志文件 主数据文件后缀名为 MDF 日志文件后缀名为 Log 如数据库Test Test mdf 与test log 2 数据库文件保存位置 1 在SQL Server Manag
  • 云计算实验——OpenStack的安装与使用

    实验目的 1 掌握Linux虚拟机的安装方法 2 掌握OpenStack的单机安装方法 3 熟悉OpenStack的核心组件 实验环境 Windows10 20H2 VirtualBox 6 1 18 r142142 Ubuntu 18 0
  • 目标检测常用特征类型提取

    本文介绍图像识别和目标检测中常用的特征 分别是Haar 哈尔 特征 LBF local binary pattern 特征 HOG histogram of orientation gradient 特征共 三种 一 Haar特征 参考链接
  • 数组根据对象id去重的几种方法

    arr id 1 name 张一 age 20 id 1 name 张一 age 20 id 2 name 张二 age 20 id 3 name 张三 age 20 方法一 通过forEach再通过some方法判断数组是否包含当前对象id
  • 【Linux】解决Linux挂载的磁盘突然没有权限修改的问题

    可能由于异常关机导致磁盘挂在错误 我这的解决办法是 gt sudo ntfsfix dev sda3 Mounting volume The disk contains an unclean file system 0 0 Metadata
  • 网站服务器发生故障,全国DNS服务器发生故障

    关键词 DNS故障 网页打不开 上不去网 DNS 网站故障 从今天下午三点左右开始中心接受用户反映故障数十起 用户均反映网页打开有问题 中心客服人员调查后发现全国出现了大范围的DNS故障 导致大量网站域名解析不正常 此次DNS故障可能是国外
  • windows

    简介 RabbitMQ是一套开源 MPL 的消息队列服务软件 是由 LShift 提供的一个 Advanced Message Queuing Protocol AMQP 的开源实现 由以高性能 健壮以及可伸缩性出名的 Erlang 写成
  • rsa生成公钥秘钥中产生的问题

    解决 module object has no attribute newkeys 1 需要导入模块rsa 自己在学习的过程中遇到了以下的错误 显示没有这个属性 解决办法 1 检查是否有rsa模块 如果没有就下载该模块 进入cmd后输入py
  • APK反编译破解方法与加密措施

    所谓APK指的是Android操作系统的应用程序安装文件 所谓Crack 简单地理解为 破解 我具体指的是反编译APK文件进行汇编级的代码分析 并修改或插入自己的代码 重新签名打包为APK文件 以达到改变程序原有行为的目的 由以上的说明可知
  • MySQL-HAVING语句

    语法 SELECT column1 column2 column n aggregate function expression FROM tables WHERE predicates GROUP BY column1 column2 c
  • Loaded runtime CuDNN library: 7102 (compatibility version 7100) but source was compiled with 7004

    我被这个cuDNN可谓坑的很惨 最开始下载了7 1 1 for CUDA9 0 跑程序的时候出现了Loaded runtime CuDNN library 7101 compatibility version 7100 but source
  • 保存textarea编辑格式到数据库,并在div中正确显示出来

    一 保存textarea编辑格式到数据库 在textarea中输入回车符 在js读取textarea中的值有 r n然后到业务层转换到string中就有可能变成空格形式然后被存入数据库 当在取出此值的时候则会变成空格的形式 因此我们需要将不
  • javaWeb监听器

    JavaWeb监听器 三大组件 Servlet Listener Filter 监听器 接口 内容由我们来实现 它需要注册 例如注册在按钮上 监听器中的方法 会在特殊事件发生时调用 观察者 事件源 事件 监听器 javaweb中的监听器 事
  • 如何画时序图

    10年产品经理教你3步画好UML时序图 轻松掌握流程分析利器 建议收藏 知乎 转自知乎 上次介绍了活动图 这次分享 UML 中 另一种流程分析利器 时序图 以前每次要分析流程 我都会用活动图 直到有一次 我面对一个业务流程 画活动图 画来画
  • 用UDP实现client程序发送字符串到server程序,server程序将字符串打印出来。

    server c include
  • Java中Scanner.useDelimiter( )方法使用

    在Java语言中 格式化输入是通过类java util Scanner来完成的 默认情况下 Scanner是使用 空白 作为分隔符将输入分解为标记 然后使用它所提供的不同的next方法将得到的标记转换为不同的类型的值 Scanner sca
  • matlab 图像压缩 奇异值分解 SVD 代码仿真实现

    首先 在对图像进行奇异值分解之前 我们应当明白SVD的原理 在矩阵原理这门课里 我们曾经学过奇异值分解 其中讲到 奇异值分解可以将一个比较复杂的矩阵用更小更简单的几个子矩阵的相乘来表示 这些小矩阵描述的是矩阵的重要的特性 在这里 我推荐对奇
  • 开源订单管理系统

    系统概述 随着企业信息化管理的不断深化 数字化技术对企业发展影响加深 为优化企业服务 最大程度提升客户体验及企业管理 开源字节与客户进行深入沟通需求 定制研发了开源订单管理系统 客户订单管理是现代企业商务业务的重要组成部分 可以帮助企业解决
  • HashMap源码-Put详解(HashMap是如何添加元素的)

    HashMap是Java中很重要一个部分 内容较多 因此笔者在此将其拆成一个个小块 作为自己学习知识整理的同时 也和广大网友一起讨论 也因此 在完成系列的学习之前 将以这种小节的形式进行学习分享 并在学习结束后进行整合 排序 一 HashM