在理解扩容器前得先明白HashMap中几个常量的含义

    //默认初始容量 ,必须为二的次幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //默认加载因子, 当前所占空间/总空间 ,若该值大于0.75 则需要进行扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

//转为树的阈值(链表转为红黑树的阈值)
    static final int TREEIFY_THRESHOLD = 8;

//加载因子,可自定义其大小
    final float loadFactor;

接下来便是扩容操作的

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) {
        //容量大于等于最大容量则返回当前的tab(不再扩容任由hash冲突,再扩容数组过大。) 
        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);
    }
    //这里是针对 初始化HashMap时定义了容量情况, 因为其他两种情况都对newThr 进行了赋值
    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;
    //开始对新扩容后的table赋值
    if (oldTab != null) {
        //遍历旧的桶中所有数据,重新计算应该将数据所放位置。(将旧的桶中数据都移动到新的桶中去)
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                //将旧桶数据置空,便于GC回收
                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;
                        //这里采用了一个非常巧妙的方法,判断该节点扩容两倍后,该节点扩容位值是0或是1来判断该节点应该是在
                        //原为,还是移动table[j+oldCap]上去,例如当前oldCap = 16,即2^4,进行扩容后直接将节点的hash值
                        //oldCap & hash 得到的就是扩展位的值是0还是1了,非常巧妙
                        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;
                    }
                    //原索引放到新表中同样索引位置+oldCap
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

HashMap扩容可以分为三种情况:

**第一种:**使用默认构造方法初始化HashMap。从前文可以知道HashMap在一开始初始化的时候会返回一个空的table,并且thershold为0。因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。

**第二种:**指定初始容量的构造方法初始化HashMap。那么从下面源码可以看到初始容量会等于threshold,接着threshold = 当前的容量(threshold) * DEFAULT_LOAD_FACTOR。

**第三种:**HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。

tab[i = (n - 1) & hash] 方式得出元素在数组中存放位置

** **n-1 即为数组最大下标值,最大小标志与key的哈希值做与运算 , 由与运算特性可知 ,得出的结果肯定是小于两者中的最小值的。而哈希值一般是比较大的一个值,因此就相当于是取余操作。

  • 那么为什么不直接用取余操作呢?
    • & 运算效率远远高于 % 运算。

扩容时候,索引位置计算原则

总结:

在进行扩容时候时,可以做到各个元素存放位置变动不大,要么是原来的位置,要么是原来索引+扩容前数组总容量。极大的减少了开销同时效率,不用rehash, 只需要判断新增那位是零还是一, 零则还是原来索引位置,一则原来索引+扩容前数组总容量即可。


标题:HashMap的扩容
作者:JonLv
地址:http://39.108.183.139:8080/articles/2023/03/11/1678547309604.html