Java之HashMap源码分析(第八篇:扩容细节分析)

(注:本文基于JDK1.8)

前言

    在Java之HashMap源码分析(第四篇:扩容机制)文章中,我已经做了扩容的源码分析,遗憾的是,扩充容量的许多细节没有提及,所以才有这篇文章的详细分析,先一起看下在什么情况下,resize()方法会在HashMap中被调用,我将依照HashMap.java源文件中resize()方法被调用的先后顺序进行描述

 

 1、添加多个元素时

HashMap添加一个Map集合中的所有元素时,如果即将添加的Map集合中的所有元素的数量大于HashMap对象持有的扩容阈值,此时resize()方法会被调用

2、第一次添加元素时

HashMap对象创建后,第一次添加元素时,resize()方法会被调用

3、添加一个新的元素后

HashMap添加一个新的元素后,如果HashMap对象已经持有的元素总数大于当前扩容阈值threshold,resize()方法会被调用

4、桶中的单链表太长需要转红黑树时

哈希冲突太多,桶内的单链表结构即将转为红黑树结构的方法中,如果HashMap对象持有的底层数组对象的长度小于64个,resize()方法会被调用

5、computeIfAbsent()方法添加元素时

通过computeIfAbsent()方法添加元素时,若当前元素总数大于扩容阈值或者底层数组仍未初始化时(table为null)或table长度为0(HashMap第一次添加元素时,底层数组才有可能为null),resize方法会被调用

 

6、compute()添加元素时

通过compute()方法添加元素,当前元素总数大于扩容阈值或者底层数组仍未初始化时(HashMap第一次添加元素底层数组才可能为null)或者底层数组table的长度为0,resize方法会被调用

 

7、合并添加的元素时

通过merge()方法合并元素时,若当前元素总数大于扩容阈值或者底层数组仍未初始化时(HashMap第一次添加元素底层数组才可能为null)或者底层数组的长度是0时,resize方法会被调用

 

共计7处调用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;
    }

resize()方法是HashMap扩容时调用的方法,所谓扩容,就是将HashMap对象持有的数组对象,由容量小的数组对象,换成一个容量更大的数组对象,这样HashMap依赖这个数组对象就能保存更多的元素了!

 

resize()方法实现过程(简述):

1、创建局部变量,保存临时数据

2、根据旧数组的容量、旧数组的扩容阈值去确定新数组容量、新数组的扩容阈值

3、创建新的数组对象,并由HashMap对象持有的实例变量table负责保存

4、旧数组不为null时,将旧数组中的所有元素全部转移到新数组中,转移过程中,会在旧数组中一个桶一个桶的遍历元素

5、如果扩容成功,则resize()方法的返回值为新创建的数组对象

 

…………以下内容为resize()方法的详细分析…………

 

一、创建局部变量,保存数据

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

局部变量oldTab负责持有旧数组对象table

局部变量oldCap负责持有旧数组的容量,当旧的数组对象oldTab还未创建时,初始值为0,如果oldTab已经创建,则获取此数组的长度

局部变量oldThr负责持有旧数组的扩容阈值threshold

局部变量newCap负责保存新数组的容量

局部变量newThr负责保存新数组的扩容阈值

 

二、根据旧数组对象的容量、旧数组对象的扩容阈值,计算出新数组的容量、新数组的扩容阈值

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

1、判断旧数组容量是否大于0,此时会出现两种情况

第一:旧数组容量大于等于最大容量(MAXIMUM_CAPACITY),则将int的最大值赋值给HashMap对象持有的当前扩容阈值threshold,然后整个resize方法会结束,并return旧的数组对象

第二:将旧数组容量oldCap左移1位,计算出的新值会赋值给新数组容量newCap,此时新数组容量newCap是旧数组容量oldCap的两倍,然后判断新数组容量newCap是否小于MAXIMUM_CAPACITY,如果为true,会继续判断旧数组容量oldCap是否大于等于默认初始化容量16(DEFAULT_INITIAL_CAPACITY),若也为true,就会将新数组扩容阈值newThr也赋值为旧数组扩容阈值的两倍

 

2、旧数组数量如果小于等于0时且旧数组扩容阈值oldThr大于0,则会用旧数组的扩容阈值oldThr赋值给新数组的容量newCap,还记得湖边的夏雨荷吗?当使用两个参数的构造方法创建一个HashMap对象时,其中有这么一句让人摸不着头脑,它对传入的初始化容量做了一个保护,找到一个与比它大,且最接近2的n次方的数字,然后把结果赋值给了threshld,这里竟然给扩容阈值threshold做了一个赋值,没想到作者会在resize方法里利用该threshld值赋值给新数组的容量newCap

 

3、最后一种情况则是数组容量小于等于0且初始阈值threshold也等于0的情况,这时新数组容量newCap赋值为默认值16,新数组阈值newThr赋值为16 * 0.75,即12

 

三、将计算好的新数组阈值newThr、新创建的数组对象newTab、分别由HashMap对象持有的实例变量threshold与table各自保存上

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

1、HashMap对象持有的实例变量threshold负责持有新数组的扩容阈值newThr

2、根据已经计算好的新数组的容量newCap,创建一个大小为newCap的数组对象,并由局部变量newTab暂时持有

3、HashMap对象持有的实例变量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;
                        }
                    }
                }
            }
        }

遍历旧的数组对象,判断桶中是否持有元素(HashMap对象持有的数组对象,此时数组的每个下标称为【桶】),旧数组桶中持有的元素会添加到新的数组中,新的数组中将包含旧数组中的所有元素(注:oldTab此时不为null,若oldTab未指向一个数组对象,resize方法将会直接返回新的数组对象newTab)

1、桶内没有元素

此时代码块结束,会遍历下一个桶

2、桶内有元素的三种情况(每次会先将旧数组的桶下标处先赋值为null,然后再根据桶内的元素情况做处理)

first、桶内是一个元素

                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;

取出桶内Node对象e的保存的hash值,与新的数组容量-1作一个按位与计算,计算的结果是一个新数组中的桶下标,利用新计算出来的桶下标将Node对象赋值到新的数组中

second、桶内是红黑树

                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

桶内取出的是TreeNode类的对象时,说明为红黑树结构

由红黑树结点对象的split方法负责完成旧数组红黑树结构迁移到新数组中的工作(参见HashMap红黑树篇)

third、桶内是单链表

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

首先定义5个局部变量:loHead、loTail、hiHead、hiTail、next,均是Node类型,lo开头的为一组、hi开头的为另外一组、next单独存在,作者为什么要定义这5个局部变量?

do……while循环开始……局部变量next负责存储当前节点对象的下一个节点对象e.next,接着会将每个当前节点对象e持有的hash与旧数组容量oldCap作一个按位与计算,按位与计算的结果会出现两种情况:0与非0

当按位与的结果为0时,由loHead、loTail负责持有单链表中的节点对象(loHead的作用是指向头结点、loTail的作用是指向尾节点)

当按位与的结果为非0时,由hiHead、hiTail负责持有单链表的节点对象(hiHead指向头结点、hiTail指向尾节点)

上面的行为,如果都有命中,那么旧数组桶中的一个单链表就会被分割成两个单链表 

在旧数组中的单链表循环结束后,loHead、hiHead分别持有的单链表会分别放到新数组中的桶中,loHead持有的单链表会放到新数组中与旧数组相同的桶下标j处,而hiHead持有的单链表则会放到新数组中的j+oldCap桶下标处(下图为可能出现的分割单链表情况展示)

 

总结

1、旧的数组容量大于MAXIMUM_CAPACITY……2^31,不会再创建新的数组对象,否则resize方法一定会返回一个新的Node数组对象

2、新创建的数组对象容量一定是旧数组的2倍

3、新的数组对象,它的扩容阈值一定是旧的数组对象扩容阈值的2倍

4、红黑树结构的处理情况,需要单开文章总结,因红黑树相对复杂……

5、较长的单链表在扩容时是可能会被分割成两个单链表的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值