【HashMap】HashMap源码put方法分析(jdk1.8超详解)

目录

 按住Ctrl点击put方法进去到源码当中(结果如下图)

 点进hash()方法查看细节

然后回到这里,点进putVal方法

一开始创建HashMap的时候肯定是未初始化的,所以查看一下具体的resize()的实现逻辑

 首先对变量进行了定义

然后,它会保存旧的table数组、旧的容量oldCap和阈值oldThr。然后,根据旧的容量来判断是否需要进行扩容操作。

如果已经初始化了在resize()中就走的是这块

之后要进行的就是将元素重新分配到table(哈希表)数组当中

首先,检查旧的table数组是否为null,并进入循环。

接下来,根据e的情况进行不同的操作

1、如果e是一个单节点(没有后续节点)

 2、如果e是一个树节点(TreeNode)

3、如果e是一个链表节点

回到putVal方法中来

1、首先定义一系列变量

2、检查哈希表是否为空或长度为0

3、计算插入位置的索引

4、处理冲突情况(当计算出的位置不为空的时候)

5、更新已存在的键值对或插入新键值对

6、更新哈希表状态和扩容(先插入后扩容)

7、调用afterNodeInsertion()方法进行插入操作后的处理。

8、返回null表示新增键值对。


 按住Ctrl点击put方法进去到源码当中(结果如下图)

 点进hash()方法查看细节

 在传入key之后进入了hash方法,在方法中定义了一个整型h(用于存储最终的哈希值)。

(key == null) ? 0 : (h = key.hashCode())

        这里是一个三元运算符,代表的意义为,检查传入的key是否为null,如果是null则直接返回哈希值0,否则继续执行下面的步骤

(h = key.hashCode())

这里调用了对象key的hashCode方法,获取到了它的哈希码,然后将结果返回给了h

再来看(h >>> 16);

这里使用了无符号右移16位,相当于把 h 的二进制表示向右截断 16 位,只保留低 16 位的值(扰动),从而增加哈希值的随机性减少冲突

(h = key.hashCode()) ^ (h >>> 16)

然后执行异或运算得到最终的哈希值

然后回到这里,点进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;
    }

首先来看

Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

这段代码主要处理数组的初始化和扩容操作。

  1. 首先,定义了一系列局部变量:tab(存储哈希表的数组),p(当前节点),n(哈希表容量),i(数组索引)。

  2. tab = table 将哈希表的数组赋值给 tab 变量。

  3. 检查数组是否为 null 或者长度为 0。如果是,则说明哈希表还没有被初始化或者已经为空,需要进行初始化或者扩容操作。

  4. 在括号中的部分 (tab = resize()) 是调用了 resize() 方法进行数组的初始化或者扩容操作,并将返回的数组赋值给 tab 变量。

  5. 然后,将 tab.length 赋值给 n 变量,这里的 n 表示数组的长度。

一开始创建HashMap的时候肯定是未初始化的,所以查看一下具体的resize()的实现逻辑

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 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);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            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;
    }

以上就是resize()方法的完整代码,其作用主要就是对HashMap进行扩容。

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

 首先对变量进行了定义

1、代码创建一个变量oldTab,并将其赋值为table数组。table数组是HashMap中存储元素的数组。

2、代码使用三元运算符判断oldTab是否为null,如果是null,则表示原来的HashMap没有任何元素,将旧的容量oldCap设置为0;否则,将旧的容量oldCap设置为oldTab数组的长度。

3、代码将阈值threshold赋值给变量oldThr。threshold是HashMap中的一个属性,表示数组需要进行扩容的阈值。

4、定义了两个变量newCap和newThr,并将newThr初始化为0。

然后,它会保存旧的table数组、旧的容量oldCap和阈值oldThr。然后,根据旧的容量来判断是否需要进行扩容操作。

 我们先来看这一部分的代码,这是一开始未初始化HashMap的时候,就走的这块

       1、 旧的容量(oldCap)为0,这表示原来的HashMap没有任何元素。在这种情况下,代码会将新的容量(newCap)设置为默认的初始容量(DEFAULT_INITIAL_CAPACITY——16),并且计算新的阈值(newThr)。新的阈值是根据新容量和负载因子(DEFAULT_LOAD_FACTOR——0.75)计算得到的。

        2、如果新的阈值(newThr)为0,说明需要重新计算新的阈值。代码中使用了float类型的变量ft存储新容量(newCap)乘以负载因子(loadFactor)的结果。然后检查新容量是否小于最大容量(MAXIMUM_CAPACITY)

        3、并且检查ft是否小于最大容量(MAXIMUM_CAPACITY),如果满足条件,则将ft转换为int类型作为新的阈值(newThr),否则将新阈值(newThr)设置为Integer.MAX_VALUE,即最大的整数值。

        4、然后,将新的阈值(newThr)赋值给HashMap的阈值(threshold)。

        5、接下来,代码创建一个新的table数组(newTab)来代替旧的table数组,这里使用了类型转换和泛型抑制警告。

        6、最后,将新的table数组(newTab)赋值给HashMap的table属性,完成了HashMap的扩容操作。

如果已经初始化了在resize()中就走的是这块

        首先,代码判断旧的容量(oldCap)是否大于0。如果旧的容量大于0,说明原来的HashMap已经有元素存在。在这种情况下,代码会进行进一步的判断和处理。

        如果旧的容量(oldCap)达到了HashMap的最大容量(MAXIMUM_CAPACITY),即oldCap >= MAXIMUM_CAPACITY,那么将阈值(threshold)设置为Integer.MAX_VALUE,即不再进行扩容操作,并直接返回旧的table数组(oldTab)。

        如果旧的容量(oldCap)小于最大容量(MAXIMUM_CAPACITY),则将新的容量(newCap)设置为旧的容量的两倍(oldCap << 1),并且进行额外的判断。

        首先,判断新的容量(newCap)是否小于最大容量(MAXIMUM_CAPACITY),以及旧的容量(oldCap)是否大于等于默认的初始容量(DEFAULT_INITIAL_CAPACITY)。如果这两个条件都满足,则将新的阈值(newThr)设置为旧的阈值(oldThr)的两倍(oldThr << 1),也就是将阈值翻倍。

        接着,如果旧的容量(oldCap)为0,说明原来的HashMap没有任何元素,只有初始阈值(oldThr)大于0时,才进行以下操作:将新容量(newCap)设置为初始阈值(oldThr)。

        作用:根据旧的容量、阈值和默认值来确定新的容量和阈值。如果旧的容量达到最大值,则直接返回旧的table数组。否则,根据条件判断来决定是否将新的容量扩大一倍,并且将新的阈值设置为旧的阈值的两倍。

之后要进行的就是将元素重新分配到table(哈希表)数组当中

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

首先,检查旧的table数组是否为null,并进入循环。

循环内部,获取旧table数组中索引位置j处的元素e。

接下来,根据e的情况进行不同的操作

1、如果e是一个单节点(没有后续节点)

  • 将旧table数组中索引位置j处的元素设为null。
  • 将该元素根据其哈希值计算新table数组的索引,并将其放入新table数组中对应位置。
 2、如果e是一个树节点(TreeNode)

  • 调用split()方法将树节点e及其子节点按照哈希值重新分配到新的table数组中。

3、如果e是一个链表节点
                    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;
                        }
                    }
  • 创建四个辅助节点:loHead、loTail、hiHead和hiTail。
  • 使用do-while循环遍历链表,并根据e的哈希值的最低位(oldCap的二进制表示为100...00)将e放入低位链表(loHead和loTail)或高位链表(hiHead和hiTail)中。
  • 循环结束后,将低位链表的头节点放入新table数组中的索引j处,将高位链表的头节点放入新table数组中的索引j+oldCap处。
if ((e.hash & oldCap) == 0) {
    if (loTail == null)
        loHead = e;
    else
        loTail.next = e;
    loTail = e;
}
while ((e = next) != null);
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}

如果 (e.hash & oldCap) == 0,将其填入到低链表当中

  • 如果低位链表的尾节点loTail为null,即低位链表还没有节点,则将当前节点e赋值给低位链表的头节点loHead。
  • 如果低位链表的尾节点loTail不为null,即低位链表已经有节点,则将当前节点e连接到loTail的后面,即loTail.next = e。
  • 最后,将低位链表的尾节点更新为当前节点e,即loTail = e。
else {
    if (hiTail == null)
        hiHead = e;
    else
        hiTail.next = e;
    hiTail = e;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}
  • 如果高位链表的尾节点hiTail为null,即高位链表还没有节点,则将当前节点e赋值给高位链表的头节点hiHead。
  • 如果高位链表的尾节点hiTail不为null,即高位链表已经有节点,则将当前节点e连接到hiTail的后面,即hiTail.next = e。
  • 最后,将高位链表的尾节点更新为当前节点e,即hiTail = e。

        然后,判断高位链表是否非空,即判断hiTail是否为null。如果高位链表非空,则将高位链表的尾节点hiTail的next指针置为null,表示链表结束。然后,将高位链表的头节点hiHead放入新table数组的索引j + oldCap处,即newTab[j + oldCap] = hiHead。

至此,resize()方法就讲解完毕

回到putVal方法中来

1、首先定义一系列变量

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

tab表示哈希表数组,p表示当前节点,n表示哈希表长度,i表示计算出的插入位置索引。 

2、检查哈希表是否为空或长度为0

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

        n = (tab = resize()).length;

        如果table(哈希表)为空或长度为0,则通过调用resize()方法进行初始化或扩容,并将返回的新数组赋值给tab。然后,更新n为新表的长度。 

3、计算插入位置的索引

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

        tab[i] = newNode(hash, key, value, null);

         通过计算(n - 1) & hash来确定插入位置的索引,并将索引赋值给i。然后,检查位置tab[i]是否为空,如果为空,则在该位置创建一个新的节点,并将其插入。

4、处理冲突情况(当计算出的位置不为空的时候)

        

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

如果位置tab[i]不为空,则表示发生了冲突。首先,定义了变量e表示当前节点,k表示当前节点的键。

  • 如果当前节点p的哈希值和键与待插入的键值对匹配,则将当前节点赋值给e
  • 如果当前节点p是树节点(通过instanceof判断),则将插入操作委托给树节点自身的putTreeVal()方法进行处理。
  • 如果当前节点p是普通链表节点,则通过循环遍历链表,寻找与待插入键值对匹配的节点:
    • 如果找到了匹配的节点e,则跳出循环。
    • 如果未找到匹配节点,则将新创建的节点插入到链表尾部,并根据链表长度是否达到阈值(TREEIFY_THRESHOLD——8),判断是否需要将链表转换为红黑树。

5、更新已存在的键值对或插入新键值对

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

        如果变量e不为空,则表示已存在相同的键值对。如果onlyIfAbsent标志为false或旧值oldValue为空,则更新该节点的值为新值value。然后,调用afterNodeAccess()方法进行额外处理,并返回旧值oldValue

6、更新哈希表状态和扩容(先插入后扩容)

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

        modCount计数加一,并判断当前大小是否超过阈值。如果超过阈值,则调用resize()方法进行动态扩容。

7、调用afterNodeInsertion()方法进行插入操作后的处理。

8、返回null表示新增键值对。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值