JDK1.8 HashMap 扩容机制

先看一下原函数的注释

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */

使用场景

Initializes or doubles table size.

resize 规律

原来某个坑位中的元素,要么还留在原来的位置,要么移动 a power of two 个位置。实际上是移动原来数组长度个位置。


整体流程

  1. newTab = new 一个二倍原长度的 Node 数组,threshold 变成两倍;
  2. 将 table (Map中存放元素的容器)指向 newTab;
  3. 依次遍历原来 Node 数组的每个坑位,将坑中的元素进行 rehash。

一个 table length 从 8 到 16 的 扩容例子

当然实际数组初始长度最小为 16, 不存在从 8 到 16 的扩容,只是为了好画图。

假设元素的 hash(key) = key

Node 节点中存储的 hash 其实就是 key 的 hash 值

亦即:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)

Node 节点定义:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        
        // otherThings
}

0. 初始状态

一个长度为 8 的数组,loadfactor = 0.75,threshold = 6.

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    int threshold;

JDK1.8 HashMap 扩容机制_第1张图片

1. 建立新table

resize() 函数会在两种情况下被调用:

  1. HashMap new 出来后还没有 put 元素进去,没有真正分配存储空间被初始化,调用 resize() 函数进行初始化;
  2. 原 table 中的元素个数达到了 capacity * loadFactor 这个上限,需要扩容。此时调用 resize(),new 一个两倍长度的新 Node 数组,进行rehash,并将容器指针(table)指向新数组。
    final Node<K,V>[] resize() {
        // 记录原 table
        Node<K,V>[] oldTab = table;
        // oldCap:原数组长度;oldThr:原table中节点个数阈值
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 原数组已经初始化过了
        if (oldCap > 0) {
            // 数组不能更大,调大threshold
            // 默认 MAXIMUM_CAPACITY = 1 << 30,Integer.MAX_VALUE 的一半
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // oldCap >= DEFAULT_INITIAL_CAPACITY 保证map数组已经初始化过了
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        // 以下是Map数组还没有初始化的情况
        // 分两种:
        // 1. 显式传入初始 capacity的时候,threhold 在构造函数中被赋值,其值为数组初始容量
        // 2. 使用无参构造函数 new HashMap,threshold = 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);
        }
        // newThr 超过了 Int 的最大值
        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 就指向了它
        table = newTab;

JDK1.8 HashMap 扩容机制_第2张图片

2. 对原数组中所有元素进行 rehash

依次遍历 oldTab 的每个坑位,对每个坑位中的元素挨个进行rehash。可分为三种情况:

  1. 原坑位中没有元素:oldTab[j] == null,不用管,保持原样就好。
  2. 原坑位中元素被树化了(单个坑位中元素个数大于8,链表变成二叉树),调用 TreeNode.split() 函数将原树分成两部分,分别装进新table的不同坑位。(这个过程中可能发生去树化———树又变回链表,树的情况本篇不做讨论)
  3. 原坑位中是一个Node链表:分成两部分,分装,详见下文。
if (oldTab != null) {
    // 依次遍历每个坑位
    for (int j = 0; j < oldCap; ++j) {
        Node<K,V> e;
        // 坑位上有元素
        if ((e = oldTab[j]) != null) {
            // oldTab 的这个坑位上元素置空,为 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
针对一个坑中的拉链进行 rehash

将链表分成两部分的过程:

(e.hash & oldCap) 是否为 0 即:Node 节点中 key 的二进制 hash 值的从右往左第 n+1 位 (2^n = oldCap) 是否为1. 因为数组长度一定是 2 的次方,其二进制值只有一位是 1,其余全为 0.

JDK1.8 HashMap 扩容机制_第3张图片

图解:

JDK1.8 HashMap 扩容机制_第4张图片

JDK1.8 HashMap 扩容机制_第5张图片
JDK1.8 HashMap 扩容机制_第6张图片
JDK1.8 HashMap 扩容机制_第7张图片

现在,原链表被分成了两部分:

JDK1.8 HashMap 扩容机制_第8张图片

  • loHead 指向的部分放在 newTab[1] 中;
  • hiHead 指向的部分放在 newTab[1+8] = newTab[9] 中。

JDK1.8 HashMap 扩容机制_第9张图片
resize 后的结果:
JDK1.8 HashMap 扩容机制_第10张图片

存疑

在 rehash 过程中,通过巧妙的方式进行新的 index 计算,达到了两个效果:

  1. 原来在一个坑位中的所有元素,被分散到 resize() 后的两个坑位中;
  2. 原来不在一个坑位中的元素,rehash 的时候也不会在一个坑位中。更确切地,原来在 a 坑中的元素 rehash 到 a1 a2 坑中,原来在 b 坑中的元素 rehash 到 b1 b2 坑中,这四个新坑不会有冲突。

这个正确性是不是需要数学证明呢?

你可能感兴趣的:(JDK,源码)