源码解析HashMap的put方法

news2024/11/28 10:38:38

前言

HashMap 基于哈希表的 Map 接口实现,其中的值是以 key-value 存储形式存在,即主要用来存放键值对。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。那么本篇文章将从源码的角度来很详细地讲解HashMap中put方法的执行流程

数据结构

在了解其真正流程前我们要先知道HashMap底层使用的数据结构,这个在不同的JDK版本中是不同的。在JDK1.8之前HashMap的底层是由数组+链表组成的。形成一个拉链型的结构。如下图:

为什么要使用这样的结构呢?因为要解决哈希冲突。

什么是哈希冲突?

哈希冲突(Hash Collision)是指在使用哈希表存储数据时,两个或多个不同的键(Key)被哈希函数映射到同一个位置的情况,因为由哈希函数所产生的哈希值是有限的,有可能会出现相同的哈希值。

因此为了解决哈希冲突,采用了这种数组加链表的数据结构,当发生哈希冲突时,会将冲突的值继续放在链表上。

而在JDK1.8之后,HashMap底层数据结构就变为了数组加链表加红黑树。

这样做到主要目的是当表中数据过大导致数组很长,链表长度过长时,查询数据就需要遍历整个链表,这样速度会很慢,因此在JDK1.8中当链表过长时链表就会自动转变为红黑树,这样可以提高查询效率。

put方法解析

下面我们来真正步入正题,我们来看HashMap在执行put方法时底层究竟是如何执行的。

初始化

首先我们进入put方法,可以发现其是调用了putVal方法。

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

该方法中有几个参数,首先是再次调用了hash方法,然后是key和value.

我们来看hash方法,其会返回一个整数值,该整数值为key的hashcode值和其无符号右移16的数,两个数做异或操作的结果。这样是为了使得到的哈希值更加分析尽量减少冲突。

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

其他的四个参数没什么要说的。

我们直接继续来看putVal方法。

该方法中代码较多,我们一步一步来看。

首先其设置了几个变量,tab和p是一个节点。

再将t现在已有table赋值给tab,并判断其是否为空,这样写可以减少很多代码。如果其为空就证明是第一次使用,并将tab设置为resize()方法的返回值,并让再把长度赋值给n。

    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;

我们继续再来看resize方法,这个方法首次使用用来初始化,当不为map不为空时用于扩容(具体如何扩容的后面会说)

首先其设置了一个oldtab变量并将其设置为一个table(第一次使用时table为空),随后判断其是否为空,如果为空就将参数oldCap设置为0,后又设置了一个oldThr阈值,也为0,这几个表示老的。然后由设置了几个新的参数newCap,newThr,同样为0.

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;

因为其容量为正常来说设置为0,因此我们忽略下面代码

       if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

继续往下看,他将新的容量设为了默认容量。新的阈值设置为了默认阈值。

else {               // zero initial threshold signifies using defaults
      newCap = DEFAULT_INITIAL_CAPACITY;
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
     }
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

我们可以看到其默认容量为1左移4位,也就是16.因此从此可知hashMap默认容量为16

而新的阈值是默认容量乘以负载因子。负载因子为0.75,那么新的阈值也就是12.

随后按照新的容量设置了一个新的节点格式的数组。并将其作为我们新的数组table设置上,然后判断了老数组是否为空,因此老数组为空,因此我们直接跳过,返回新数组。

        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //下面if中的都跳过,下面用来进一步扩容的
        //if (oldTab != null) {
          //  for (int j = 0; j < oldCap; ++j) {
            //    Node<K,V> e;
              //  if ((e = oldTab[j]) != null) {
                //    oldTab[j] = null;
                  //  if (e.next == null)
                    //    newTab[e.hash & (newCap - 1)] = e;
                    //else if (e instanceof TreeNode)
                      //  ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //else { // preserve order
                      //  Node<K,V> loHead = null, loTail = null;
                        //Node<K,V> hiHead = null, hiTail = null;
                        //Node<K,V> next;
                        //do {
                          //  next = e.next;
                            //if ((e.hash & oldCap) == 0) {
                              //  if (loTail == null)
                                //    loHead = e;
                                //else
                                  //  loTail.next = e;
                                //loTail = e;
                            //}
                           // else {
                             //   if (hiTail == null)
                               //     hiHead = e;
                               // else
                                 //   hiTail.next = e;
                                //hiTail = e;
                         //   }
                        //} while ((e = next) != null);
                        //if (loTail != null) {
                         //   loTail.next = null;
                       //     newTab[j] = loHead;
                        //}
                        //if (hiTail != null) {
                          //  hiTail.next = null;
                           // newTab[j + oldCap] = hiHead;
                        //}
                    //}
                //}
            //}
        //}
        return newTab;

这样我们就拿到了我们新构建的一个数组。

以上的步骤都是在我们当前的数组为空的情况下进行的,因此这一步我们其实在给底层数组进行初始化。

put新值且没有发生哈希冲突

随后我们继续来看purVal下面的代码

首先时第一种情况:我们put新值且没有发生哈希冲突

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

在这一步中将数组中第(n-1与传的hash值做与操作得到的值)个数字的值将其赋给p,如果这个p为空,那么就在此处放置一个新的节点并放置了key,value。

这一步通俗讲就是如果这个位置为空,那么就将要要插入的节点放到此处。

为什么要怎么选择这个位置?

因为我们要通过hash值来快速找到我们要插入的位置,会根据我们二进制hash值得后几位进行插入。

put的key已经存在

第二种情况,put的key已经存在

继续往下看,如果当前这个位置不为空已经有了节点。就会走下面的方法。

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

首先设置了一个新的节点e,

如果我们p处已有节点的hash值等于我们传的hash值,并且key也相等,那么直接将p设置给e。很显然这次的情况就是我们给同一个key做了两次put操作。 

随后再往下,如果此时的e不为空就将e的值赋给老值,再将e的值设置为我们的传进来的新值,最后返回老值,这样就相当于我们直接把值给覆盖了。

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

因此以上的情况就是我们put了一个已经有了的值,我们用新值覆盖了旧值。

put新的key且发生了哈希冲突

下面继续来看不等,也就是put了一个新的key且发生了哈希冲突的情况

现在这种情况走的就是下面的代码.

代码中首先先开始遍历,当遍历到最后一个节点后的那个空节点时将该节点设置为我们的新节点(附带key,value),相当于插入了一个节点。

当插入完后会进行判断,如果插入后的长度大于最大的长度8时,调用转化为红黑树的方法,即treeifyBin方法。

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

所以我们现在来看一下treeifyBin方法,可以看到进入方法后会先进行一个判断,如果当前底层数组为空或者长度小于MIN_TREEIFY_CAPACITY(此值为64)时会进行扩容。否则会将链表转化红黑树

因此从这里我们可以知道hashmap中链表转化为红黑树的条件

hashmap会在链表长度超过8并且数组长度超过64时将链表转化为红黑树.

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

现在我们再回到resize方法中查看是如何扩容的

下面代码在进行扩容

       if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

可以看到扩容时会进行判断,如果现有容量已经大于等于最大容量则不会再继续扩容,会将阈值设为最大值并将已经容量直接返回。(一般达不到这么大超过最大容量)

如果没有达到最大容量则会正常进行扩容,将oldCap左移一位,也就是扩大2倍。

扩大完后,我们再次遍历一边老的数组,对于数组中每一个不为空的值,下一个如果为空,则会将这个空值放入新数组对应的位置(用hash决定)当中。

如果是红黑树会用split方法将其拆开。

再其次,会将这整条链表拆为低链和高链。

那么是如何判断哪些节点在高链,哪些节点在低链呢?

我们是根据原节点的hash值和老容量做与操作,如果为0是低链,如果为1为高链。

然后低链留在当前位置,高链去我们当前容量加上老容量的对应位置。这样就完成了扩容。

        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;

如果不需进行扩容,就会正常将链表转化为红黑树

也就是下面代码,下面代码比较简单就不再多做介绍了。

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

还有一种情况,如果是在增加链表时的出现了已经存在key值,也同样会进行覆盖,直接return old节点,和上面时一样。

最后提一点,每次添加完节点和都会进行容量++(无论时链表拉长还是红黑树中添加节点),覆盖除外。

至此,本篇文章的全部内容正式结束,我感觉我在这里面对于源码的解读已经很详细,解释了hsahmap在put时的过程以及会发生的所有情况。

希望我的总结会对你产生一定的帮助,帮助你更好地理解hashmap.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1587003.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

5G智慧水利数字孪生可视化平台,推进水利行业数字化转型

5G智慧水利数字孪生可视化平台&#xff0c;推进水利行业数字化转型。随着5G技术的快速发展&#xff0c;越来越多的行业开始探索数字化转型的道路。水利行业作为国民经济的重要支柱&#xff0c;也面临着数字化转型的迫切需求。5G智慧水利数字孪生可视化平台作为水利行业数字化转…

微服务-7 Docker

一、镜像、容器、仓库 容器是镜像的实例&#xff0c;仓库中存储着镜像。 二、镜像的操作 三、容器的操作 创建容器停止容器&#xff0c;查看后发现没有了(docker ps 默认只展示没有停止的) docker ps -a (可以展示运行中和停止的镜像)删除容器&#xff1a;(docker rm 不能删除…

LangChain-18 Caching 将回答内容进行缓存 可在内存中或数据库中持久化缓存

背景描述 可以将问答的内容缓存起来&#xff0c;如果是相同的问题&#xff0c;那么将会直接把答案返回去&#xff0c;可以节约费用和计算。 安装依赖 pip install -qU langchain-core langchain-openai编写代码 我们可以通过 InMemoryCache 进行内存缓存 或者 SQLiteCache …

【C++题解】1605. 求一个两位数的个位和十位的和

问题&#xff1a;1605. 求一个两位数的个位和十位的和 类型&#xff1a;基本运算、拆位求解。 题目描述&#xff1a; 从键盘读入一个两位的整数 n &#xff0c;请求出这个两位整数个位和十位的和是多少&#xff1f; 输入&#xff1a; 一个两位的整数 n 。 输出&#xff1a…

R语言ggplot2绘图学习笔记(基础知识大全)

R语言ggplot2绘图入门笔记 今天分享的内容是在R语言中利用ggplot2进行可视化的入门笔记&#xff0c;适用于初学者了解ggplot2绘图系统。干货满满&#xff0c;建议收藏&#xff01; 首先安装以下R包&#xff1a; install.packages(c("tidyverse", "colorspace&qu…

机器学习-09-图像处理01-理论

总结 本系列是机器学习课程的系列课程&#xff0c;主要介绍机器学习中图像处理技术。 参考 02图像知识 色彩基础知识整理-色相、饱和度、明度、色调 图像特征提取&#xff08;VGG和Resnet特征提取卷积过程详解&#xff09; Python图像处理入门 【人工智能】PythonOpenCV…

PostgreSQL强势崛起,选择它还是MySQL

大家好&#xff0c;关系型数据库&#xff08;RDBMS&#xff09;作为数据管理的基石&#xff0c;自数据仓库兴起之初便扮演着核心角色&#xff0c;并在数据科学的发展浪潮中持续发挥着价值。即便在人工智能和大型语言模型&#xff08;LLM&#xff09;日益成熟的今天&#xff0c;…

【方法】PDF密码如何取消?

对于重要的PDF文件&#xff0c;很多人会设置密码保护&#xff0c;那后续不需要保护了&#xff0c;如何取消密码呢&#xff1f; 今天我们来看看&#xff0c;PDF的两种密码&#xff0c;即“限制密码”和“打开密码”&#xff0c;是如何取消的&#xff0c;以及忘记密码的情况要怎…

2024考研调剂须知

----------------------------------------------------------------------------------------------------- 考研复试科研背景提升班 教你快速深入了解掌握考研复试面试中的常见问题以及注意事项&#xff0c;系统的教你如何在短期内快速提升自己的专业知识水平和编程以及英语…

ArcGIS Desktop使用入门(三)图层右键工具——缩放至图层、缩放至可见

系列文章目录 ArcGIS Desktop使用入门&#xff08;一&#xff09;软件初认识 ArcGIS Desktop使用入门&#xff08;二&#xff09;常用工具条——标准工具 ArcGIS Desktop使用入门&#xff08;二&#xff09;常用工具条——编辑器 ArcGIS Desktop使用入门&#xff08;二&#x…

php-redis windows ,pecl 已经不维护了,解决方案:php 8.2 | 8.3+ redis extension windows

从论坛上pecl 已经不维护了&#xff0c;直接让大家到ci 去下载 https://stackoverflow.com/questions/76496488/redis-dll-not-found-for-php8-2/76496489#76496489 让我们找最新的一次commit &#xff0c;然后又action 构建&#xff0c;再下载&#xff0c;这样的话也好&#…

【STM32嵌入式系统设计与开发】——17ADC(ADC按键应用)

这里写目录标题 STM32资料包&#xff1a; 百度网盘下载链接&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1mWx9Asaipk-2z9HY17wYXQ?pwd8888 提取码&#xff1a;8888 一、任务描述二、任务实施1、工程文件夹创建2、函数编辑&#xff08;1&#xff09;主函数编辑&#…

DTC 子故障类型说明

在配置cdd的时候&#xff0c;需要知道子故障类型&#xff0c;例如一个DTC &#xff1a;0xC128187, 这个0x87就是子故障类型&#xff0c;是一种常见的故障类型&#xff0c;对手件timeout

微商商城源码小程序好用么?

商城APP作为电子商务行业的重要组成部分&#xff0c;已经成为了人们购物的主要方式之一。为了在竞争激烈的市场中脱颖而出&#xff0c;开发一款专业且思考深度的商城APP方案显得尤为关键。本文将从专业性和思考深度两个方面&#xff0c;探讨商城APP的开发方案。 一、专业性的重…

计算机网络常见面试总结

文章目录 1. 计算机网络基础1.1 网络分层模型1. OSI 七层模型是什么&#xff1f;每一层的作用是什么&#xff1f;2.TCP/IP 四层模型是什么&#xff1f;每一层的作用是什么&#xff1f;3. 为什么网络要分层&#xff1f; 1.2 常见网络协议1. 应用层有哪些常见的协议&#xff1f;2…

jetbrains全家桶历史版本下载(IDEA/PyCharm...)

IDEA&#xff1a; 其他版本 - IntelliJ IDEA (jetbrains.com)https://www.jetbrains.com/zh-cn/idea/download/other.html PyCharm: 其他版本 - PyCharm (jetbrains.com)https://www.jetbrains.com/zh-cn/pycharm/download/other.html Goland: 其他版本 - GoLand (jetbrain…

TQ15EG开发板教程:在MPSOC上运行ADRV9371

首先需要在github上下载两个文件&#xff0c;本例程用到的文件以及最终文件我都会放在网盘里面&#xff0c; 地址放在本文最后。首先在github搜索hdl选择第一个&#xff0c;如下图所示 GitHub网址&#xff1a;https://github.com/analogdevicesinc/hdl/releases 点击releases…

python+django电子资源类学习资料分享网站flask

本设计是电子资源类的网站设计与实现&#xff0c;。系统前台实现了首页&#xff0c;分享资源&#xff0c;交流论坛&#xff0c;公告信息&#xff0c;个人中心&#xff0c;后台管理等功能。系统的后台实现了首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;分享资源管…

Android零基础入门(一)配置环境和安装Android Studio

闲来无事学一下Android&#xff0c;本人目前java为主&#xff0c;jdk的环境就不赘述了 配置环境 Java JDK5 或 以后版本 Android SDK Java运行时环境&#xff08;JRE&#xff09; Android Studio 你可以从 Oracle 的 Java 网站&#xff1a;JDKJava SE下载下载最新版本的 Jav…

基于PyAutoGUI图片定位的自动化截图工具--jmeter部分

1、计划 压测完成后需要编写性能测试报告&#xff0c;报告中所需数据截图较多&#xff0c;使用自动化操作方便快捷&#xff0c;就编写一个界面工具以便后续复用。之前编写过loadrunner报告的自动化截图脚本&#xff0c;现在用jmeter也比较多&#xff0c;就编写jmeter部分&#…