认真研究HashMap的初始化和扩容机制

关联博文
数据结构之Map基础入门与详解
认真学习Java集合之HashMap的实现原理
认真研究HashMap的读取和存放操作步骤
认真研究HashMap的初始化和扩容机制
认真研究JDK1.7下HashMap的循环链表和数据丢失问题

本文我们总结HashMap在jdk1.8/jdk1.7下的初始化和扩容机制。

【1】jdk1.8下

① 初始化

HashMap的初始化可以有如下几种方式:

 public HashMap(int initialCapacity);
 public HashMap() ;
 public HashMap(Map<? extends K, ? extends V> m)
 public HashMap(int initialCapacity, float loadFactor)

loadFactor是负载因子,默认是0.75。在实例化HashMap时可以指定初始容量。如果你指定了初始容量,那么就会触发tableSizeFor(initialCapacity);方法,其将会计算threshold值。

其目的就是是将传进来的参数转变为2的n次方的数值然后赋予threshold。>>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0。这样就是保证了HashMap容量必须是2的整数幂,比如传入12那么得到的threshold为16。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

关于移位保证2的N次幂图示可以参考博文:HashMap之tableSizeFor方法图解

根据HashMap的构造函数可以发现,此时其核心成员transient Node<K,V>[] table还是null,并未被初始化。指定初始化容量只是对阈值threshold进行了初始化。

② 扩容

扩容的场景

  • putMapEntries时如果HashMap.size>threshold,触发扩容;
  • putVal时如果tab未被初始化,则触发扩容;
  • putVal后如果++size > threshold则触发扩容;也就是整个HashMap中元素个数大于阈值,就会触发扩容
  • treeifyBin链表转化为红黑树时如果tab.length<MIN_TREEIFY_CAPACITY(64),则触发扩容;
  • computeIfAbsent 或 compute 或 merge时如果size > threshold || (tab = table) == null ||(n = tab.length) == 0触发扩容;

代码如下所示,这里先说明一点,扩容的本质是获取新的容量和新的临界值,并对链表或者树进行转化。

final Node<K,V>[] resize() {
	//旧的数组
    Node<K,V>[] oldTab = table;

    //获取旧的容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //旧的阈值
    int oldThr = threshold;

    //新的默认为0
    int newCap, newThr = 0;

    //旧的容量大于0,说明不是第一次扩容
    if (oldCap > 0) {
	    //如果旧的容量大于最大容量上限,则直接返回旧的数组
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // newCap =oldCap*2  ; newThr =oldThr*2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
   
    //说明初始化时指定了容量,则 newCap = oldThr
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 无参构建,第一次put,newCap =16, newThr =12
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
 
    // 为newThr 赋值,要么是newCap * loadFactor;,要么是Integer.MAX_VALUE
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //更新HashMap的成员threshold 
    threshold = newThr;
	//实例化大小为newCap的Node 数组并赋予table 
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    
    //如果oldTab不为null,则要把原先桶内元素放到新的桶内
    if (oldTab != null) {
    	//对桶内每一个元素进行遍历,元素可能是单一结点,可能是链表,可能是树结点
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
            // 获取位置 j 结点,并将旧的赋予null
                oldTab[j] = null;
                if (e.next == null) //单一结点直接放进去,(n-1)&hash确定位置
                    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;
// 通过(e.hash & oldCap) == 0来判断是否需要将链表拆分为两个链表
                        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) 对链表的处理

如上所示,这里使用(e.hash & oldCap) == 0来判断是否需要拆分链表。如果结果为0,则位置不变,(链表的头结点)仍旧是newTab[j]。如果结果不为0,则(链表的头结点)放到newTab[j + oldCap]位置处。

这里会遍历旧链表的每个结点,通过判断来尝试得到两个链表并记录两个链表的头结点。

那么这里有两个问题:为什么使用(e.hash & oldCap) == 0来判断?为什么改变的位置是newTab[j + oldCap]

前面我们定位key的索引位置使用的方法是(n-1)&hashn 表示hashMap的容量(oldCap)也就是tab.lengthhash表示key的散列值。也就是说(oldCap-1)&hash相等的key并不意味着(e.hash & oldCap)计算结果也相等,所以存在了拆分链表的可能性(而且只可能拆分为两个链表)。

为什么使用(e.hash & oldCap) == 0来判断?

因为(e.hash & oldCap) == 0,那么位置必然不会发生变化!

我们举例如下。假设oldCap为16,那么扩容后为32。使用(n-1)&hash对一个key来说唯一区别在于高位的1(扩容前后低位都是1),这也就是为什么使用e.hash & oldCap。如果与这个高位的1进行&运算得到0,表示key的hash在高位这个1位置是0,那么扩容前后进行索引位置运算结果不会发生变化!

32 0010 0000
31 0001 1111
16 0001 0000
15 0000 1111

这也能够解释为什么只可能拆分为两个链表了,因为与高位1进行&运算,结果只有两种。


为什么改变的位置是newTab[j + oldCap]

前面我们提到过,扩容的时候(newCap = oldCap << 1),也就是newCap =2*oldCap 。假设oldCap为16,那么扩容后newCap就是32。也就是在(oldCap-1)&hash(newCap-1)&hash的运算过程中,这个差别仅仅在于后者二进制位高位比前者多了一个1,这个1换算成十进制也就是oldCap。

与高位1的&运算,结果要么是0,要么是1。前者位置不变,后者位置+oldCap

如下所示四组key分别在新旧数组中的位置,可以看到位置发生改动的 newIndex=oldIndex+olcCap

key(hash)oldCap=16newCap=32
key1(16)016
key2(32)00
key3(48)016
key4(64)00

(2) 对树结点的处理

对树结点的处理如下所示,首先从当前结点开始遍历next,同上述链表处理一致。尝试获取两棵树(一棵位置改变,一棵位置+oldCap)。与链表不同的时,扩容的时候还会判断树的结点数量是否满足<=UNTREEIFY_THRESHOLD = 6,如果是,则将树结点转换为链表。

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

// int bit = oldCap   int index = j
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order

    //数组索引位置没有发生变化的
    TreeNode<K,V> loHead = null, loTail = null;

    //数组索引位置发生变化的
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        //(e.hash & oldCap) == 0
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;//通过这两步形成链表
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
// 到这里for循环结束,也是件检索整个树结点
    if (loHead != null) {
    	// UNTREEIFY_THRESHOLD = 6
        if (lc <= UNTREEIFY_THRESHOLD)
        	// 转为链表
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            //如果hiHead 为null,则说明所有节点位置没有发生变化,
            //其本身就是一棵树,无需再次处理
            if (hiHead != null) 
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
    	//转为链表 tab[index + bit]其实就是tab[j+oldCap]
        if (hc <= UNTREEIFY_THRESHOLD)
        	//转为链表
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
            	//转为树,思路同上
                hiHead.treeify(tab);
        }
    }
}

关于树结点的转化我们另起篇章说明。整体扩容流程如下所示:
在这里插入图片描述

【2】Jdk1.7下

① 初始化

HashMap的几个构造方法如下所示:

public HashMap(int initialCapacity, float loadFactor)public HashMap(int initialCapacity)public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m)

与jdk1.8不同的是,实例化时就初始化了threshold 和capacity 并由此 创建了数组table(这样就存在了空间浪费),如下所示:

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

    // Find a power of 2 >= initialCapacity
    // 这里使用了while,jdk1.8采取的位运算
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 实例化时就为数组分配了空间
    table = new Entry[capacity];
    useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    init();
}

② 扩容

在这里插入图片描述

如下所示在put方法中新增结点可能会触发扩容,newCapacity=2*table.length,也就是2倍扩容。
在这里插入图片描述

resize方法

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    
    //对旧容量做了最大值判断,如果已经达到最大值则更新threshold ,直接return
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

	//实例化新数组,容量为newCapacity
    Entry[] newTable = new Entry[newCapacity];
    
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    
    // 数据的转化-旧数组的数据转到新数组中
    transfer(newTable, rehash);
   
    //table指向新数组
    table = newTable;
   
    //计算并更新threshold 
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

transfer方法

jdk1.7.0_17中HashMap的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);
            }
            // h & (length-1) 计算当前结点在新数组中的位置
            int i = indexFor(e.hash, newCapacity);
            
            //头插法,会形成倒排效果
            e.next = newTable[i];
            newTable[i] = e;

            //更新 e 为下一个结点
            e = next;
        }
    }
}

我们以如下为例来描述一下数组的转换过程。再次强调一下,(oldCap-1)&hash(newCap-1)&hash不一定相等!
在这里插入图片描述
① 首先获取到e=a, Entry<K,V> next=b,a.next=newTable[i](假设i是0,此时a.next=null),newTable[0]=a,e=b。

在这里插入图片描述

② 此时e=b, Entry<K,V> next=c,b.next=newTable[i](假设i是0,此时b.next=a),newTable[0]=b,e=c。
在这里插入图片描述
③ 此时e=c, Entry<K,V> next=null,c.next=newTable[i](假设 i 是16,发生位置改变,此时c.next=null),newTable[16]=c,e=null,将结束本次循环。跳到位置 1 处…
在这里插入图片描述
由于jdk1.7是遍历过程中对每一个结点进行了重新定位,那么并发扩容下是否会存在问题?我们在另一篇博文说明。

【3】仔细分析tableSizeFor方法

可以发现tableSizeFor方法保证了无论指定了什么initialCapacity,都会返回一个2^n的数。当在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)

下面我们仔细分析tableSizeFor方法。

static final int tableSizeFor(int cap) {
      int n = cap - 1;//①
      n |= n >>> 1;//②
      n |= n >>> 2;//③
      n |= n >>> 4;//④
      n |= n >>> 8;//⑤
      n |= n >>> 16;//⑥
      return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//⑦
  }

① cap-1

这是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。

n |= n >>> 1

无符号按位右移运算符。左操作数按位右移右操作数指定的位数,符号位跟着移动,空出来的高位补0(左边补充的值永远为0,不管其最高位(符号位)的值)。

先进行无符号右移一位(相当于n/2),然后再与n进行|运算(输入2个参数,a、b对应位只要有一个为1,c对应位就为1;反之为0)。

由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如000011xxxxxx。

n |= n >>> 2;

注意,这个n已经经过了n |= n >>> 1; 操作。假设此时n为000011xxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如00001111xxxxxx 。

④ n |= n >>> 4;

这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如00001111 1111xxxxxx 。

以此类推

注意,容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就31个1(int最大正数值),但是这时已经大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY

static final int MAXIMUM_CAPACITY = 1 << 30;

图例如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值