Hashmap的扩容机制及扩容后元素迁移·

一.HashMap基础
HashMap继承了AbstractMap抽象类,实现了Map,Cloneable,Serializable接口。
HashMap的源码属性:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    //初始容量为16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
    //负载因子默认值
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    //桶上的链表长度大于等于8时,链表转化成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    
    //桶上的红黑树大小小于等于6时,红黑树转化成链表
    static final int UNTREEIFY_THRESHOLD = 6;
    
    //当数组容量大于64时,链表才会转化成红黑树
    static final int MIN_TREEIFY_CAPACITY = 64;
  1. 初始容量:默认为16,在第一次put时生成
  2. 加载因子:默认为0.75
  3. 阈值:阈值=容量*加载因子。默认12,当元素数量超过阈值时便会触发扩容
  4. 最大容量:230 ,当容量超过230 ,容量不再扩大,阈值变为231 -1
  5. java1.8+,桶上的链表长度大于等于8时,链表转化成红黑树
  6. java1.8+,桶上的红黑树大小小于等于6时,红黑树转化成链表
  7. java1.8+,只有当数组容量大于64时,链表才会转化成红黑树

二.何时触发扩容
一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。
注意:
HashMap的容量必须为2的n次幂,进行扩容时其容量变为不小于指定容量的2的幂数(即初始化容量过程)。
例如:进行有参初始化时:
new HashMap<>(5)–>初次put时,容量扩为23 =8,数组长度达到阈值6,扩容为16
new HashMap<>(7)–>初次put时,容量扩为23 =8,数组长度达到阈值6,扩容为16
new HashMap<>(13)–>初次put时,容量扩为24 =16,数组长度达到阈值12,扩容为32
new HashMap<>(19)–>初次put时,容量扩为25 =32,数组长度达到阈值24,扩容为64
三.扩容机制
当HashMap决定扩容时,会调用HashMap类中的resize()方法进行扩容。
java1.7下扩容机制
HashMap的底层结构在java1.7版本之前是:数组+单链表。
resize()源码:

   void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//当原有table长度已经达到了上限,不再扩容。
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

源码可见,当原table容量没有到达最大,则会新建一个新的数组,并调用transfer()方法进行元素迁移(见下)。

  • 第一次put时,容量扩容初始化:其容量变为不小于指定容量的2的幂数(默认为16)
  • 不是第一次put时,扩容:新容量=旧容量 * 2 ,新阈值=新容量 * 加载因子

元素迁移
transfer()源码:

 void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;            
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity); 
                e.next = newTable[i];                  
                newTable[i] = e;                  
                e = next;                              
            }
        }
}

在准备好新的数组后,map会遍历数组的每个“桶”,然后遍历桶中的每个Entity,重新计算其hash值(也有可能不计算),找到新数组中的对应位置,以头插法插入新的链表。
注意:
因为是头插法,因此新旧链表的元素位置会发生转置现象。
元素迁移的过程中在多线程情境下有可能会触发死循环(无限进行链表反转),因此HashMap线程不安全。
java1.8+扩容机制
HashMap的底层结构在java1.8版本之后是:数组+单链表/红黑树。
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;
    }

java1.8与java1.7扩容时容量的计算方法都为扩大为原来容量的二倍。
注意:
因为1.8版本之后HashMap的底层结构为:数组+单链表/红黑树。因此如果某个桶中的链表长度大于等于8了,则会判断当前的hashmap的容量是否大于64,如果小于64,则会进行扩容;如果大于64,则将链表转为红黑树。
元素迁移
java1.8+在扩容时,不需要重新计算元素的hash进行元素迁移。
而是用原先位置key的hash值与旧数组的长度(oldCap)进行"与"操作。

  • 如果结果是0,那么当前元素的桶位置不变。
  • 如果结果为1,那么桶的位置就是原位置+原数组 长度

值得注意的是:为了防止java1.7之前元素迁移头插法在多线程是会造成死循环,java1.8+后使用尾插法
注意:
java1.8 扩容的时候会判断当前的桶的位置有没有链表或者红黑树,如果没有链表或者红黑树,那么当前元素还是和JDK1.7中的求法一样,求新的桶的位置。如果有链表,那么链表的元素会按照上述方法求新的桶的位置。如果是红黑树,则会调用split()方法,将红黑树切分为两个链表,之后进行扩容操作。

posted @ 2022-09-24 18:20  郭慕荣  阅读(2170)  评论(0编辑  收藏  举报