HashMap扩容机制(JDK1.8)-- 源码鉴赏与启发

目录

一、几个重要的变量

二、HashMap扩容方法resize()分析

三、启发


一、几个重要的变量

1.默认初始化容量:    

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    默认的初始化容量大小 - 必须是2的幂。1.8版本初始容量为【16】,即初始数组大小为16。
     
2.最大容量:

    static final int MAXIMUM_CAPACITY = 1 << 30;

    哈希表数组的最大容量,即自动扩容时的容量上限。即使是使用显式构造函数指定更大的容量值,实际情况也不会超过该值。该最大容量可以通过显式构造函数指定。

3.默认负载因子:

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    当前容量 * 负载因子 = 当前负载量

    如果当前的存储的k-v键值对数量超过【当前负载量】,就会触发哈希表数组扩容,容量变为原来的2倍。该负载因子可以通过显式构造函数指定:HashMap(int initialCapacity, float loadFactor),源码如下:


    /**
     * Constructs an empty HashMap with the specified initial
     * capacity and load factor.
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            //【注意此处】
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

==>> 变量定义源码如下:


    /**
     * The default initial capacity - MUST be a power of two.
     * 默认的初始化容量大小 - 必须是2的幂。1.8版本初始容量为16,即初始数组大小为16。
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

二、HashMap扩容方法resize()分析

        扩容时最重要的问题是:旧数据重新分配存储位置。下图为哈希数组存储示意图(这里只分析链表形式):

HashMap扩容机制(JDK1.8)-- 源码鉴赏与启发_第1张图片

         HashMap存储数据过程中遇到哈希冲突时,就会产生链表。哈希数组扩容时,所有数据(数组中的数据和所有链表中的数据)都需要重新分配存储位置,方法就是hash取模。 

小白:啊?还要重新分配啊?数据量级较大时,一定很耗时吧

HashMap:因为我是按照2倍扩容的,一定范围内,当数据越多,遇到扩容的几率越小,所以不必过分担心啦~

小白:哦,链表中的节点也要重新分配,可不可以为整条链表分配位置呢?这样还节省时间。

HashMap:不行的!不行的!因为原来分配的位置已经不能满足扩容后的寻址规则了呀,对hash取模时的数组长度发生了变化。懂?

小白:那样会导致什么结果呢?

HashMap:会导致找不到数据!或者取到错误的数据。总之,会出大乱子!!!

小白:那链表中的数据按什么规则重新分配呢?也像数组中的节点那样重新hash取余定位吗?

HashMap:重新hash取余定位当然可以。但是这里有个更加巧妙高效的方法。

小白:快说快说...

HashMap:就是直接将hash值与原数组容量值(注意:它是2的n次幂)做“与”运算,然后判断是否为0。这实际上就是判断hash与原数组容量的商是否为偶数。是偶数的,分到一条链表a上;是奇数的分到一条链表b上。(为什么?)链表a还放在原来索引位置上,链表b放在原索引偏移oldCap(原数组容量)的位置。(这又是为什么?)

下面我们来思考扩容前后,两个模(余数)之间存在什么关系:

对同一个hash值,做如下证明:
设:扩容前数组容量为a时:
m:商(整数)
n:余数(整数)
则 m * a + n = hash ; n范围是[0,a)

扩容后数组容量为2a时:
y:商(整数)
z:余数(整数)
则 y * 2a + z = hash ; z范围是[0,2a)

m * a + n = y * 2a + z
m * a + n = 2y * a + z
得到 (m - 2y) * a = z - n

所以,两余数之差z-n必定是a的整数倍,
又因为n范围是[0,a),z范围是[0,2a),

所以,z-n只能为0或a。

        结论:扩容前后,分别对hash取模,两个模要么相等,要么相差一个原数组容量oldCap。

到这,明白上面两个“为什么”了吧,好,我们来看看源码具体是怎么写的吧(看看我们能不能得到些许启发):

    /** JDK1.8
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //【当超过最大容量时,不会再扩容,直接返回原node数组】
            if (oldCap >= MAXIMUM_CAPACITY) {
                //【扩容阈值设置为int的最大值】
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //【如果原数组容量默认初始化容量,且2倍容量没有超过最大容量限制,则扩容到原来的2倍】
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold 左移1位,变为2倍
        }
        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[] newTab = (Node[])new Node[newCap];
        table = newTab;

//================ 【华丽的分割线 - 好戏在后头】 ================//

        //【如果原数组非空,则进入真正的扩容逻辑;否则,无需扩容,创建数组即可】
        if (oldTab != null) {
            //【遍历原数组每个节点,每个节点都可能是一个链表或者一棵树,主要是解决哈希映射索引时的冲突问题】
            for (int j = 0; j < oldCap; ++j) {
                Node 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)e).split(this, newTab, j, oldCap);
                    
                    //【链表存储形式:需要重新分配存储位置】
                    else { // preserve order
                        //低位链表
                        Node loHead = null, loTail = null;
                        //高位链表
                        Node hiHead = null, hiTail = null;
                        Node next;
                        //【遍历该节点对应的链表,根据哈希值确定每个节点是否】
                        do {
                            next = e.next;
                            //【哈希值和原数组容量做“与”运算,由于数组容量是2的n次幂,
                            // 所以等价于 (int)(e.hash/oldCap)%2 == 0 
                            // 即hash中包含奇数个or偶数个原数组容量值,
                            // 是偶数的分到低位链表中,是奇数的分到高位链表中】妙啊~~
                            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;
                            //妙啊~【注意:为什么是偏移原数组大小oldCap呢?】
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

三、启发

1.使用了好多“位与”运算和“左移”运算,取模求余从未出现“%”。

2.这个【2的n次幂】用的简直不要太棒,极大的方便了计算,更方便了位运算的使用。

思考:如果没有使用【2的n次幂】这个规则,还能使用位运算取模吗?

===== 源码版本:JDK1.8 =====

你可能感兴趣的:(源码阅读,Java,java,源码分析,HashMap)