面试必问系列之你必须知道的HashMap扩容

308 阅读3分钟

前面我们已经看过HashMap中put方法的内部实现流程啦,那么今天我们就来一起看看resize()方法到底做了些什么吧!我们继续来看下JDK 1.8中HashMap resize()方法的源码:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
  			//获取当前tab的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0
  			//判断容量是否大于0
        if (oldCap > 0) {
          	//判断容量是否超过最大容量,如超过最大容量,则将阈值设置为最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
          	//判断将当前tab容易扩大一倍后是否小于最大容量并大于等于初始容量
          	//如果满足,则将阈值扩大一倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
  			//如果当前tab容量为0并且阈值大于0,则将新容量设置为阈值
        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) {
          	//遍历节点数组,将节点移入新的节点数组table
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                  	//如果该位置只有一个节点则计算index索引并移入
                    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;
                         		//重新计算索引位置
                         		//e.hash & oldCap == 0 存入lo链,否则存入hi链
                            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);
              					//将lo链存入当前位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
              					//将hi链存入oldCap+j位置
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

相信很多小伙伴看到e.hash & oldCap这句话很费解,但相信看了下面的例子后,小伙伴们就能明白了。

首先,我们假设oldCap为16,当前j=3,此时这个位置的链表上有A、B两个节点,这两个节点的(n-1)&hash=3,那么我们是不是可能理解为下面的情况:

hash(A): 0000 ... 0000 0011

hash(B): 0000 ... 0001 0011

&

n-1(15): 0000 ... 0000 1111

结果:

A: 0000 ... 0000 0011

B: 0000 ... 0000 0011

那么 oldCap&hash 也就是 n&hash,是不是可以如下理解呢


hash(A): 0000 ... 0000 0011

hash(B): 0000 ... 0001 0011

&

n(oldCap): 0000 ... 0001 0000 结果: A:0 B:16

newCap-1: 0000 ... 0001 1111 结果: A:3 B:19 (oldCap+j)

通过这样一对比,大家发现没有,容量扩大一倍后计算位置与旧tab的索引index存在一定联系。


当 oldCap & hash = 0 时,新表位置与旧表保持一致

当 oldCap & hash != 0 时,新表位置为oldCap+j

在扩容的时候通过这种方式,将一个链表合理的拆分成为了两部分,还省去了重新计算hash值的时间。这个设计是不是很精妙呢,反正我已经给作者献上了膝盖了。