HashMap数组扩容后元素的前后变化

  前一段时间看了HashMap 的扩容方法,觉得写的太好了,对我很有帮助,现以我理解的来写一下。主要说两方面:

  1. 扩容后元素的位置
  2. 扩容后元素如何分布的
1、resize方法的源码

  HashMap 中扩容方法为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;
    }

  上边是整个resize() 方法的源码,今天说的是其中的这部分,即从上边带有注释的说明① 位置下边的if (oldTab != null) { 这一行开始。
  从这一行的判断里看到,意思是原有table 里如果没有元素的话,会走的逻辑,其实就是hashMap 针对于数组长度达到负载因子*数组长度 的时候,会进行数据的重新分布,这一步上也是性能消耗的地方。

2、扩容后数组元素的位置(无链)

  先来看上边源码的说明② 位置,即下边这行:

if (e.next == null)
   newTab[e.hash & (newCap - 1)] = e;

  这行的意思是,如果当前数组下标上有元素,但是没有子元素,也就是没有形成链,就只有一个元素 ,那么该元素放置的位置按照newTab[e.hash & (newCap - 1)] 来放置。

  首先来看一张图,hashMap 中,确定元素在数组哪个位置,是通过hash 值(通过一个hash方法进一步运算过的)与数组长度减一进行与运算 得到的,如下图所示:

hashmap扩容会造成outofmemery hashmap扩容之后位置_resize方法


  做法特别巧妙,那如果扩容后,比如对上边的数组长度16 ,现在变成了长度为32 ,也就是上边提到的这个newTab[e.hash & (newCap - 1)] ,相同的三个数,在扩容后的放置位置是什么呢?看下图:

hashmap扩容会造成outofmemery hashmap扩容之后位置_resize方法_02


  为什么会出现不同的位置呢,请仔细看那两个hash数,对应的100000 中的1 上的数,有的有1 ,有的有0 ,因为这是个随机数,并不知道到底是几,反正不是0 ,就是1 了,这样就会使算出来的下标,分到两个位置上,使扩容后的数组,对元素又均匀的分摊一下。

  总结:HashMap 扩容后,原来的元素,要么在原位置,要么在原位置+原数组长度 那个位置上。

hashmap扩容会造成outofmemery hashmap扩容之后位置_数组_03

2、扩容后元素的位置(有链)

  借用别人的图,如下:

hashmap扩容会造成outofmemery hashmap扩容之后位置_hashMap数组扩容后元素的位置_04


  通过上图来看,左边是扩容前 ,右边是扩容后 ,对于下标为15 上整条链上的元素,扩容一倍后,元素要么在原位置15 上,要么在原位置加原数组长度,即15+16 那个位置上。并且整条链上的元素,不管在原位置15,还是在31 上,它们的排列顺序没有变化。(据说这与1.7版本不同,我没看过1.7版本)

  那是如何做到的呢,再继续看上边的源码,位置在上边源码的说明③ 处,即下边这个位置:

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

  上边的代码中,根据说明③ 处的(e.hash & oldCap) == 0 来将链上的元素分成两份,然后又在说明④ 处,对元素分别放到了原位置原位置+原数组长度 上。

  用几个数据来举个例子,就会如下图所示:

  比如一个链上的数据如下图,三种颜色,形成了链。

hashmap扩容会造成outofmemery hashmap扩容之后位置_数组长度_05

  如果对于它们三个,扩容一倍后,就会变成下边这样:

hashmap扩容会造成outofmemery hashmap扩容之后位置_hashMap数组扩容_06