首页 > 试题广场 >

说一说HashMap的扩容机制

[问答题]
推荐

得分点

​ 三个条件、翻倍扩容

参考答案

标准回答

​ 向HashMap中添加数据时,有三个条件会触发它的扩容行为:

  1. 如果数组为空,则进行首次扩容。
  2. 将元素接入链表后,如果链表长度达到8,并且数组长度小于64,则扩容。
  3. 添加后,如果数组中元素超过阈值,即比例超出限制(默认为0.75),则扩容。

​ 每次扩容时都是将容量翻倍,即创建一个2倍大的新数组,然后再将旧数组中的数组迁移到新数组里。由于HashMap中数组的容量为2^N,所以可以用位移运算计算新容量,效率很高。

加分回答

​ 在数据迁移时,为了兼顾性能,不会重新计算一遍每个key的哈希值,而是根据位移运算后(左移翻倍)多出来的最高位来决定,如果高位为0则元素位置不变,如果高位为1则元素的位置是在原位置基础上加上旧的容量。

​ 举个例子,来演示一下数据迁移时,元素在新数组里位置的判定。假设旧数组长度为16(00010000),则翻倍后新数组的长度为32(00100000)。我们从十进制数字上看不出什么,但是从二进制数字却可以看出二者的明显差别,即翻倍后新值的高位多了1。

​ 如果我们把这两个值作为掩码,对key的哈希值做按位与运算,就能求出最高位那一位的差异。如果这一位是0则该元素的位置不变,否则该元素的位置就在原位置基础上加16。这个方式很巧妙,它省略了重新计算哈希值的时间,同时新增出来的一位是0或1是随机的,这样就把产生冲突的节点均匀的分散到新的槽里了。

hash & (16-1) = hash & 00001111
hash & (32-1) = hash & 00011111

延伸阅读

​ 如下图,数据迁移的过程,可以参考槽15内的元素的迁移路线进行理解:


图片说明

​ 另外,HashMap的扩容是通过resize()方法实现的,该方法的关键代码如下:

final Node<K,V>[] resize() {
    ...
    // 迁移数据
    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 {
                    // 将链表中节点拆分到新数组的低位和高位
                    // 低位的头尾节点
                    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) {
                            ...
                        }
                        // 追加到高位链表
                        else {
                            ...
                        }
                    } 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;
}
编辑于 2021-09-15 10:31:43 回复(0)
1、HasMap初始化的时候,会指定初始化大小,以及扩展阀值,如果不指定,就使用默认值,此时数组为null;
2、向HashMap中存值的时候,如果数组为null或者为空,开始第一次扩容,创建初始化大小的数组,默认为16
3、如果当链表的长度达到8,同时数组的长度小于64,进行扩容
4、如果当hashMap的元素总量达到了阀值,进行扩容
5、扩容每次将容量翻一番
发表于 2022-02-18 10:09:35 回复(0)
  • 在执行put操作时,如果数组中的元素超过阈值时,会对数组进行扩容,每次扩容的大小都为之前的两倍 。初始的数组大小为16,负载因子为0.75,因此初始的阈值大小=数组大小 * 负载因子 = 12。扩容时通过创建一个新的数组来代替原来的数组,并将原数组中的元素迁移到新数组中。
  • 在实现上JDK 1.7与1.8存在一些区别。
  • JDK 1.7在链表中插入元素时采用的头插法,因此在数据迁移的时候可能产生元素位置倒置的现象。在多线程情况下还可能触发循环死链与数据错乱的问题。JDK 1.7在当前数组大小大于等于阈值且没有空位时才会进行扩容。
  • JDK 1.8在插入元素时采用的是尾插法,且当前数组大小大于阈值时立刻进行扩容。JDK 1.8还对迁移时计算hash索引的方法进行了优化,效率更高。
发表于 2022-06-28 18:43:12 回复(0)
这个题是不是太无聊了。。。真有人这样问?
发表于 2022-11-28 18:48:46 回复(0)
1.当初始化hashmap时数组为null当第一次push时会初始化容量为16.
2.当前容量达到阈值以前的0.75时 执行扩容每次是之前的2倍,将链表通过尾插法复制过去避免并发产生死链
3.底层使用hash表的数据结构,数组+链表+红黑树 当单条链表的长度>8并且数组长度》64时就会进行树化
发表于 2023-10-10 14:33:31 回复(0)
到达阈值后扩容。新数组是旧数组的两倍,因此相当于左移1位。旧节点迁移时,只要计算新移动的那一位是不是1,不是保留在原来的index,是则index = oldindex+oldCapacity
发表于 2023-06-19 00:52:36 回复(0)
首先要了解Hashmap中几个重要的属性:
int initialCapacity: Hashmap初始容量,默认为16
float loadFactor: Hashmap负载因子,默认为0.75
int threshold: 触发HashMap亏扩容的阈值,当容量到达这个阈值的时候,需要对HashMap进行扩容。(等于capacity * load factor

HashMap的扩容机制由 HashMap class中的 resize()负责。在 HashMap 中,resize() 方法不仅仅承担了 table 的扩容,它还承担了 table 的初始化。

1.  New HashMap
当创建一个新的HashMap class的时候,只会初始化HashMap的初始容量及其相关配置(initialCapacity,loadFactor,threshold),并不会初始化hash array。当第一个元素被放进HashMap的时候,会触发resize() 初始化hash array(Lazy load)。

2. 在HashMap中插入元素的过程
  • 判断数组是否为空,若为空则调用resize()初始化;
  • 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
  • 查看 table[index] 是否存在数据,没有数据就构造一个 Node 节点存放在 table[index] 中;
  • 存在数据,说明发生了 hash 冲突(存在二个节点 key 的 hash 值一样), 继续判断 key 是否相等,相等,用新的 value 替换原数据(onlyIfAbsent 为 false);
  • 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
  • 如果不是树型节点,创建普通 Node 加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树;
  • 插入完成之后判断当前节点数是否大于阈值,如果大于则调用resize()扩容为原数组的二倍。

3. 扩容函数resize()内部逻辑
  • 判断hash array是否初始化,未初始化则会根据配置先初始化新的Entry array。
  • 若已经初始化,则创建一个新的 Entry 空数组,长度是原数组的 2 倍,
  • 遍历原数组,对每个元素重新计算新数组的索引值,然后放入到新数组的对应位置。

发表于 2022-11-04 22:37:58 回复(0)
  1. hashmap初始化时会指定初始化大小以及扩展阈值,如果不指定就使用默认值为null。

  2. 当向hashmap中存值时,

    如果为就开始第一次扩容,创建初始化数组大小,默认为16,

    如果当前长度大于8且数组长度大于64时,将链表转为红黑树结构,

    如果添加的元素达到了阈值的0.75,就进行扩容(默认加载因子是0.75,比如16的容量,就会在16*0.75=12的时候进行扩容)

    扩容使用resize方式,每次扩容都是之前的两倍,是使用新数组代替原=原来的旧数组,将原数组的数据迁移到新数组中使用尾插法。

发表于 2022-09-25 14:56:42 回复(0)
1.hashMap初始化时会制定初始化大小 添加元素就会扩容 ,初始数组大小为16
2.当元素添加到链表上,链表长度大于8,数组长度64则扩容
3.元素追加达到了阈值0.75,则扩容,扩容2倍

扩容方法是用一个新数组代替了原来的数组 将原数据迁移到新数组中使用尾插法

发表于 2022-04-23 13:38:16 回复(2)
1.如果你没有指定初始长度的话,默认会为null,这时候往里面添加元素他就会进行扩容,初始数组大小为16.
2.当元素添加到链表上,链表的长度大于8的时候,数组长度小于64时,会将链表转换成红黑树
3.当添加元素达到了他的阈值,默认的加载因子是0.75,比如16的容量,你加到12的时候他就会进行扩容。
扩容是resize方法,每次扩容都是两倍,他是用一个新的数组代替了原来那个小的数组,将原数组的数据迁移到新数组里面。

发表于 2022-03-28 11:10:54 回复(1)

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值, 即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。 
扩容( resize )就是重新计算容量,向 HashMap 对象里不停的添加元素,而 HashMap 对象内部的数组无法装载更 多的元素时,对象就需要扩大数组的长 度,以便能装入更多的元素。当然 Java 里的数组是无法自动扩容的,方法 是使 用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想 装更多的水,就得换大水桶。
发表于 2022-02-17 17:38:06 回复(0)
当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值, 即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
扩容( resize )就是重新计算容量,向 HashMap 对象里不停的添加元素,而 HashMap 对象内部的数组无法装载更 多的元素时,对象就需要扩大数组的长 度,以便能装入更多的元素。当然 Java 里的数组是无法自动扩容的,方法 是使 用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想 装更多的水,就得换大水桶。
发表于 2022-01-14 19:33:48 回复(0)