hashMap的底层机制和源码

本文详细探讨了HashMap的底层实现,包括其数组+链表的结构,以及扩容机制。在Java 8中,当链表长度超过8时,HashMap会将链表转换为红黑树。在源码分析中,阐述了put操作的过程,包括哈希计算、冲突解决以及树化条件。文章还介绍了HashMap的初始化、扩容和冲突解决的具体步骤。
摘要由CSDN通过智能技术生成

hashMap的底层机制和源码

1、hashMap的底层示意图

在这里插入图片描述

hashMap的底层实际上是数组+链表的形式,这个数组的一个值就是实现了Map E n t r y 的接口 H a s h M a p Entry的接口HashMap Entry的接口HashMapNode, 通过源码知道,Entry是Map里边的一个内部接口,而HashMap N o d e 是 H a s h M a p 里边的一个静态内部类,而且实现了 M a p Node是HashMap里边的一个静态内部类,而且实现了Map NodeHashMap里边的一个静态内部类,而且实现了MapEntry接口
在这里插入图片描述
在这里插入图片描述

2、hashMap的扩容机制

  1. HashMap底层维护了个Node类型的table,默认为null;
  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75 , 这个是用来干嘛的呢?用来作为触发扩容机制的条件,比如当前数组大小为16,当加入的数据个数到达了 16* 0.75 = 12 的时候,就会对当前的容量进行扩容。
  3. 当添加key-val时,通过key的哈希值来得到在table中的索引位置,然后判断该索引处是否存在元素,如果没有元素就直接添加,如果该索引处有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val; 如果不相等需要判断是树结构还是链表结构,如果是链表结构,依次遍历链表,看看有没有相同的key,有相同的key的话就将val进行替换,否则就把节点加入到链表后边,当然还要考虑到链表是否需要转化成树结构;如果是树形结构,也要进行相应处理,下边我会说到。
  4. 第一次添加,就需要扩容table容量为16,临界值(threshold)为12,这个也就是触发扩容机制的临界值。
  5. 以后在扩容,则需要扩容table容量为原来的2倍,当然,临界值也变成了原来的两倍,即24,依次类推。
  6. 在java8中,如果一条链表的元素超过 TREEIFY_THRESHOLD(默认是8),并且table的大小>= MIN_TREEIFY_CAPACITY(默认64),就会转化为树形结构(红黑树)

3、hashMap的源码分析

首先我们写一段简单的hashMap的存值的代码

public class MapResource {

    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("java", 10);
        map.put("php", 10);
        map.put("java", 20);
        System.out.println(map);
    }
}

当然输出结果为:{java=20, php=10}

我们进行一个debug

在这里插入图片描述

在执行hashMap的构造方法时候就初始化了加载因子(loadFactor = 0.75)

在这里插入图片描述

然后开始执行put方法

在这里插入图片描述

这里边有一个hash(key)的方法,主要就是用来计算key的hash值,然后通过这个hash值来定位到在table表上的位置,这个比较的好理解。

这里我们可以进入hash(key)的方法看一下实现,反正就是返回一个int类型的hash值。

在这里插入图片描述

然后进入putVal()的方法:

在这里插入图片描述

这个方法就是hashMap存值的核心代码,看着那么多ifelse 脑壳有点疼,我们可以一步一步来分析。

首先:

Node<K,V>[] tab; Node<K,V> p; int n, i;   //这个就是定义一些辅助变量,这个没什么好说的

我们第一次加数据的时候 ,很显然我们的table表是null的,而且长度是0, 因此会进入到这个if分支,主要会执行resize()这个方法,这个方法其实就是初始化table:

if ((tab = table) == null || (n = tab.length) == 0)  //table表为null,并且长度为0
            n = (tab = resize()).length;

我们可以进入到resize()这个方法中去看看

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSWmKsBp-1658152071722)(hashMap的底层机制.assets/1658067907567.png)]

我们可以看到 这个table数组中每个元素都是一个Node<K,V> 结构的数据,然后进行下一步

//这一步是用来判断通过这个hash值获取索引,通过索引在table表上找到具体的位置,然后比较这个位置是不是为空的,当然,我们这里存值第一次进来,首先初始化table表大小后,显然所有的位置都是null,自然会进入这个分支。
if ((p = tab[i = (n - 1) & hash]) == null) 
            tab[i] = newNode(hash, key, value, null);

可以很清楚的看到,就是新创建一个Node节点节点挂在tab[i] 下,至于这个newNode()方法我们也可以进去看一下。

在这里插入图片描述

实际上就是初始化了一个Node节点并返回,Node节点其实就是一个链表的数据结构:

在这里插入图片描述

第一次加入数据的时候,table表什么都没有,那么就很顺利的将值放入table中,然后继续看源码,可以知道,代码执行到

++modCount;    //修改次数+1
        if (++size > threshold)  //判断是否需要扩容,看看是否大于临界值,显然这一次size才1,不会触发扩容机制
            resize();
        afterNodeInsertion(evict);
        return null;

然后第一次的存值就完成了。

然后进行第二次的存值,第二次我们的key是php,通过计算,hash值显然和java不一样,那么就会进入到

//同样的,计算后的hash值在table表上找到的索引位置不一样,而且当前位置没有元素,为null,那么就创建一个新的节点挂在上边。
if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

接下来就是就是执行下边这段代码了。

++modCount;    //修改次数+1
        if (++size > threshold)  //判断是否需要扩容,看看是否大于临界值,显然这一次size才1,不会触发扩容机制
            resize();
        afterNodeInsertion(evict);
        return null;

然后我们进行第三次存值,第三次我们存入的key是java,和第一次一样,那么计算得到的hash值就是一样的,肯定在table表上的索引位置就是一样的,此时我们来看源码。

在这里插入图片描述

当出现hash冲突的时候,就会走到这个分支下,而且有三种情况,我们来具体分析一下

Node<K,V> e; K k;    //这个也是辅助变量,没什么好说的

第一种情况:

在这里插入图片描述

//能够进入这里的原因就是和之前第一次添加的java的hash值一样,并且是同一个对象或者你的key的equals方法一样,那么就将e指向p
if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

第二种情况:

在这里插入图片描述

//就是说,如果这个table表当前索引位置的数据的数据结构是树机构,那么用树的处理方法来解决,通过这个putTreeVal方法来处理。具体里边干了什么,就不展开细说了。
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;
                }
            }

一进来就是一个for循环,还没有循环次数条件,是一个死循环,就是说找到了,但是这个节点后边挂着的是链表,新加入的这个节点就会去比对这个链表的每一个节点。

  • 如果在比对的过程中找到了和新加入的节点的key相同的节点,也就是循环到了这段代码:
//这个判断条件是不是很眼熟,就是和第一种情况的一样。在比对节点的时候发现新加入的节点的key和比对的节点的key一样,那么就结束循环,我们可以发现,在上次循环的时候,p已经指向了e了,也就是说,在结束循环的时候,已经找到了需要处理的节点在链表中的位置。
if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
  • 还有一种情况,如果节点从头到尾比对完了还没有找到怎么办呢?这个时候就会走到这段代码
if ((e = p.next) == null) {   //相当于就是在末尾了,p.next == null了
                        p.next = newNode(hash, key, value, null);  //这段就很眼熟了,就是在链表后边新加入一个节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

加节点也不可能一直在链表的后边加节点,前面说到过,当节点的长度到达8的时候,就会进行树化,于是会执行下边这个方法

在这里插入图片描述

//加入后,判断当前链表的个数,是否已经到8个,到8个后,就调用treeifyBin 方法进行红黑树的转化。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st   TREEIFY_THRESHOLD = 8
                            treeifyBin(tab, hash);

但是调用这个方法一定就进行树化吗?其实还有条件才能进行树化,我们可以点开treeifyBin() 方法进行查看

在这里插入图片描述

也就是说,如果当前table表为null或者table表的长度小于64,那他只会执行resize() 方法对table表进行扩容,而不会马上树化,综上所述,只有当某一个节点链表的长度大于8了,并且table表的长度大于64了,那么这个链表才会转化为红黑树。

最后,不管你是树化也好,还是找到新的节点也好,在循环中都会将p指向e

在这里插入图片描述

然后跳出循环以后,就会来到下面这串代码

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

这段代码我们可以清楚的看到,就是替换值的一个过程。

上边就是HashMap存值的全部过程了。
在这里打个小广告,欢迎访问我的个人博客:https://www.xiaoger.top

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值