Java之HashMap源码分析(第四篇:扩容机制)

(注意:本文源码基于JDK1.8)

 

前言

    HashMap中有多达7处通过调用resize()方法进行扩容操作,接下来我们一起分析用于扩充数组容量的resize()方法,学习它的实现思想……加油……

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

    用于将HashMap对象持有的数组对象容量进行扩容的resize()方法,该方法会返回一个新的数组对象,HashMap对象会持有这个新的数组对象,且新的数组对象中会包含旧数组对象中保存的所有元素,以此完成扩容数组容量的目的,resize()方法虽然比较长,但是我们可以将其分解成一个一个的步骤,接下来我们一起对resize()方法的源码进行详细的分析……

一、准备阶段

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

1、创建局部变量用于存储HashMap对象持有的当前数组对象

实例变量table是HashMap对象持有的底层数组对象,此时赋值给创建的局部变量oldTab负责保存,相对即将用于扩容而创建的新数组对象而言,我们称它为旧数组

2、创建局部变量用于保存旧数组对象的容量

判断局部变量oldTab是否为null,如果为null,说明底层数组对象并没有创建过,此时为局部变量oldCap赋值为0。当底层数组对象已经创建,oldTab则不为null,获取旧数组对象的容量,并赋值给局部变量oldCap

3、创建局部变量用于存储扩容阈值

HashMap对象持有的实例变量threshold表示底层数组是否需要扩容的阈值,当数组中添加的元素总数大于threshold的值时,当前数组对象就需要进行扩容,此处将threshold赋值给局部变量oldThr保存,保存下旧数组的扩容阈值

4、创建两个局部变量,用于保存新的数组对象长度、以及新的扩容阈值

创建的两个局部变量,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-else的选择分支语句下,对三种情况分别作出处理:

第一种情况:当前数组对象的容量oldCap大于0

oldCap是HashMap对象持有的当前数组对象的容量

1、针对旧数组容量oldCap大于常量MAXIMUM_CAPACITY的情况

将扩容阈值threshold赋值为Integer的最大值MAX_VALUE,然后直接返回旧数组的容量,这种情况已经无法再扩充数组容量了

2、正常扩大两倍容量的情况

先将旧数组容量oldCap左移1位,获得的容量值为原先的2倍

将2倍的数组容量赋值给newCap

对newCap的值进行判断,第一个条件是检查是否小于MAXIMUM_CAPACITY、第二个条件是检查旧数组容量oldCap是否大于等于默认容量DEFAULT_INITIAL_CAPACITY(默认容量为16)

第一和第二,两个条件都满足的情况下,使用旧数组阈值oldThr左移1位,得到双倍数赋值给新数组的扩容阈值newThr

第二种情况:数组对象的容量等于0且扩容阈值大于0

将旧数组的扩容阈值oldThr作为新数组newCap的容量,下面这行代码是HashMap对象创建时,做的threshold的初始化工作,在这里终于用到了threshold值

第三种情况:旧数组对象的容量等于0且旧数组对象扩容阈值等于0的情况,初始阈值为零表示使用默认值(说明HashMap对象的创建方式一定有多种)

使用默认容量DEFAULT_INITIAL_CAPACITY作为新数组的容量,将其赋值为newCap的值

使用默认加载因子DEFAULT_LOAD_FACTOR与默认容量DEFAULT_INITIAL_CAPACITY相乘,计算得出新数组容量的阈值,并赋值给newThr

三、处理新数组的扩容阈值为0时的情况,再次计算出新的阈值

        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }

创建局部变量ft,用于存储计算出来的扩容阈值,新数组容量newCap与HashMap对象持有的loadFactor相乘,赋值给ft

新数组容量newCap小于最大容量MAXIMUM_CAPACITY、ft小于最大容量MAXIMUM_CAPACITY的情况,ft赋值给新的数组阈值newThr,反之为newThr赋值为整型最大值MAX_VALUE

四、将局部变量的值全部赋值到HashMap对象持有的扩容阈值threshold、持有的底层数组table

        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;

1、将计算好的扩容阈值进行赋值

为HashMap对象持有threshold进行赋值,局部变量newThr赋值给threshold

2、创建一个指定容量的Node数组对象

创建好的Node数组对象赋值给局部变量newTab

3、将创建的数组对象进行赋值

为HashMap对象持有的实例变量table进行赋值,局部变量newTab赋值给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;
                        }
                    }
                }
            }
        }

对旧数组对象中每一个下标处(桶处)可能出现的三种情况分别进行处理

1、桶内一个元素

2、桶内是红黑树

3、桶内是单链表

针对不同的情况,使用不同的处理方式,一起学习一下

第1行:判断旧数组对象是否存在,oldTab不为null表示存在旧数组

第2行:当旧数组存在时,开始遍历旧数组,起始下标j为0,结束遍历的条件为下标j等于旧数组容量oldCap时(意味着旧数组中的每一个元素都会被遍历)

第3行:先定义一个临时变量e,用于存储旧数组的每个桶时获取到的Node对象

第4行:从旧数组的桶下标j处取出一个Node对象,将Node对象赋值给临时变量e,接着马上判断e是否指向一个存在的Node对象,若不存在,即e==null,则for循环里面后面的语句都不会再执行,接着继续j+1,开始取出底层数组下一个桶中的Node对象

第5行:第4行为true时执行,因为旧数组下标j处的Node对象已经被临时变量e持有了,所以这里为旧数组oldTab的下标处赋值null,把该强引用断开,后面便于GC回收

第6-10+行:这里就是精彩的代码部分了,对于从旧数组某一个下标j取出来的Node对象,取出Node.next的值,然后马上判断Node.next的值,这里对象3种情况分别进行处理

第一种情况是next == null 代表该桶只有一个Node

第二、三种情况是若next不为null,桶内持有的可能是一个红黑树结构(e instanceof TreeNode)、或者是单链表结构;

红黑树结构,则会进一步调用TreeNode的split()方法,split接受4个参数,依次代表:当前HashMap对象、新数组对象、下标j、旧数组对象);

单链表结构则会将原来的单链表有可能分割成两个单链表,一个记为低位链表loHead、另一个记为高位链表hiHead,最后分别插入到原数组的j下标处、以及j+oldCap处,这个写的太精妙了,并没有为单链表中的每一个元素重新进行rehash的计算!而且大佬很重视细节,loTail.next和hiTail.next均最后还又赋值为null,因为取出来的元素,很可能原来的next值是指向下一个元素的,这样就能保证形成的两个单链表,最后一个元素的next都指向为null!

六、返回创建的新数组对象

        return newTab;

细节1:红黑树结构TreeNode的split()方法

        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

用于将红黑树结构转换为单链表结构的方法,阈值是6,为啥是6?红黑树的分析得单独开文章了,太复杂了……

细节2:单链表的遍历

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

将旧数组对象中的桶中的元素挪到新的数组对象中……,如果桶中的数据结构是单链表,则将单链表分成两个单链表,分别存储到新数组对象中的两个桶中,作者是怎么想到的?真牛逼(桶中持有单链表的头结点即可,竟然还做一个拆分)

总结

1、HashMap扩容机制,本质上是创建一个新的Node数组对象,然后再将旧的Node数组对象中的所有元素,全部挪到新的数组对象中

2、每个需要从旧数组对象中挪动的Node元素,并没有重新计算它持有的称为hash的值,而是使用Node对象持有的hash原值与新的数组容量进行计算,得到一个新的桶地址(哈希地址,数组下标)

3、当桶内是单链表结构时,可能会将单链表分成2个,然后再将2个单链表分别存储在新数组中的两个桶内(不同的下标处),一低一高(旧数组中的元素并不是一个元素一个元素的复制到新数组中的,而是只把单链表的头结点赋值过去即可)

4、为什么单链表转红黑树的阈值是8?为什么红黑树转单链表阈值则是6?

5、为什么单链表转红黑树,要加容量64的限制?

显然扩容篇没有学的很透彻……大佬太牛逼!!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值