HashMap源码解析

107 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

大家好,我是尚影嫣🌷,一名Java后端程序媛。如果您喜欢我的文章,欢迎点赞➕关注❤️,让我们一起成为更好的我们~🥰

image.png

HashMap源码解析

resize( )扩容方法

resize( )是HashMap中扩容的方法,当HashMap中存的数据量大于threshold时,或进行初始化HashMap的时候会进行扩容的操作,会调用resize()方法进行扩容。

执行resize( )方法有两种情况:

  • 在HashMap的putVal( )方法中,会先判断table是否为空,为空会执行resize( ),再初始化table。
  • 在HashMap中的存储的数据量大于threshold时,会执行resize( )方法。
/**
 * 对原table进行扩容,并返回扩容后的Node数组即HashMap的最新数据
 */
final Node<K,V>[] resize() {
    //table赋予oldTab作为扩充前的table数据
    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;
        }
        //若新表大小(oldCap*2)小于数组极限大小 并且 老表大于等于数组初始化大小
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            //旧数组大小oldThr,经二进制运算向左位移1个位置,即oldThr*2当作新数组的大小
            newThr = oldThr << 1; // double threshold
    }
    //若原table中下次扩容大小oldThr大于0
    else if (oldThr > 0)
        //将oldThr赋予控制新表大小的newCap
        newCap = oldThr;
    else { //若其他情况则将获取初始默认大小
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //若新表下一次扩容大小为0
    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
    table = newTab;
    if (oldTab != null) {
        //若oldTab中有值需要通过循环将oldTab中的值保存到新表中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //获取老表中第j个元素 赋予e
            if ((e = oldTab[j]) != null) {
                //并将老表中的元素数据置Null
                oldTab[j] = null;
                //若此判定成立 则代表e的下面没有节点了
                if (e.next == null)
                    //将e直接存于新表的指定位置
                    newTab[e.hash & (newCap - 1)] = e;
                //若e是TreeNode类型
                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循环 获取新旧索引的节点
                    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;
}

总结

  1. 数组在扩容时,容量会扩为原来的两倍。
  2. 在扩容过程中,会先创建⼀个长度为原来两倍新的table数组,同时旧table中链表会进行重哈希,根据hash后得到的不同结果,形成两条链表。一条放在数组下标与原来相同的下面,另一条放在原来的旧数组下标值+扩容的长度的下面。