为什么阿里巴巴修正了HashMap关于1024个元素扩容的次数

引言

最近在翻看《阿里巴巴开发手册-嵩山版》即最新版时,发现其修正了关于HashMap关于1024个元素扩容的次数 在先前的版本泰山版我们可以看到以下描述:

而最新版嵩山版则可以看到:

同时我们也可在嵩山版的版本历史中看到对以上变化的描述:

并且我在官网文档中也同样发现了采访视频对这一变化,主持人对孤尽老师提出了以下疑问:

主持人:

有同学问,嵩山版修正了HashMap关于1024个元素扩容的次数?那么这个泰山版中是七次扩容,我感觉是正确的,为什么现在进行了修改?

孤尽老师:

大家可以看到嵩山版描述都改了,就没有讲「扩容」,讲叫「resize」的次数。因为resize的这个次数的话,我们在调resize的时候,就put,如果你new了一个HashMap它并不会给你分配空间,第一次put的时候,它才会给你分配空间。所以就是说我们在第一次put的时候,也会调resize。但是很多同学就会争议,说这个算不算扩容?那其实就是说我们在这个点上,其实为了,因为计算机我们大家都是搞计算机的,都希望用没有「二义性」的语言来表示。所以在嵩山版本里面进行了修改,然后我们改成了resize的方式来说明这到底是它的容量是如何变化的。因为扩容这个词的话,大家的理解是不一样的。这也是我们有一次大概在业界我们有一个打卡就是一个打卡测试题,这个题里面我们有一个选项。当时选的同学是两种答案,但是这两种答案都是有自己的道理。所以我们也是借鉴了这个看法,然后在这个嵩山版里面也写的更加明确了。就是叫resize的次数,不叫扩容的次数。

以下是文档下载链接和采访视频地址。

嵩山版Java开发手册-阿里云开发者社区 (aliyun.com)

我们可以从孤尽老师的回答中提炼出主要发生的变化是:

  • 扩容次数修改为resize次数。主要的问题是每个人对「扩容」这个定义存在分歧。
  • 扩容的主要分歧是在:没有给HashMap进行初始化的时候,第一次put的时候才会分配空间,也会调用resize。那这操作个算不算扩容?即初始化容量这个操作算不算扩容?
  • 所以扩容次数修改为resize次数。

那么我们先来看一下第一次put的时候调用resize这个操作是什么?

第一次put调用resize()

前面孤尽老师的回答中也强调了这个put()方法调用resize()方法是存在一个条件的:

如果你new了一个HashMap它并不会给你分配空间。同时文档中也写了由于没有设置容量初始大小。

除了这个条件其实还有一个额外的条件,就是这是在JDK1.8之后HashMap才会有的操作。由于篇幅有限,对比JDK1.7和JDK1.8HashMap的源码可以看我之前的文章。Java不得不知道的八股文之哈希表 - 掘金 (juejin.cn)。以下源码都来自JDK1.8

要理解这句话我们首先要看下HashMap的源码,看下它的构造器方法。

存在四个构造方法

java 复制代码
//参数:初始容量,负载因子
public HashMap(int initialCapacity, float loadFactor) {  }  

//参数:初始容量。调用上一个构造函数,默认的负载因子(0.75)
public HashMap(int initialCapacity) {  this(initialCapacity, DEFAULT_LOAD_FACTOR);  }  

//参数:无参构造器。创建的HashMap具有默认的负载因子(0.75)
public HashMap() {  this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted  }  

//参数:使用与指定Map相同的映射构造一个新的HashMap。创建的HashMap具有默认的负载因子(0.75)和足够在指定Map中保存映射的初始容量。
public HashMap(Map<? extends K, ? extends V> m) {  
    this.loadFactor = DEFAULT_LOAD_FACTOR;  
    putMapEntries(m, false);  
}

也就是说当我们使用一个无参构造器创建HashMap的时候

java 复制代码
    Map<String,String> map = new HashMap<>();

调用的就是上述构造方法的第三个方法,只会设置默认的负载因子为0.75。就没有其他操作了。。

而当我们对这个HashMap进行put()操作的时候

java 复制代码
    Map<String,String> map = new HashMap<>();
    map.put("first","firstValue");

我们看一下源码,进行了什么操作?

java 复制代码
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

其实调用的是内部的putVal()方法。同时我们也可看见我们前面的构造器方法中除了设置了负载因子,就没有其他操作了,所以是符合if ((tab = table) == null || (n = tab.length) == 0),大家也可debug断点打在这进行验证。所以就会执行下面的代码n = (tab = resize()).length;调用了一次resize(),同时对n进行了赋值操作。

那么我们通过源码的确发现了,在JDK1.8中由于没有设置HashMap容量初始大小 ,在第一次进行put()操作的时候,HashMap会调用一次resize()方法初始化容量为16。(resize的源码也不是本篇文章的重点,所以我这边会直接提供结论,并且再次强调一下JDK1.7是会初始化容量为16的,而不是在是第一次进行put()操作时初始化容量。更多HashMap详情大家可以看这篇文章Java不得不知道的八股文之哈希表 - 掘金 (juejin.cn)

调用resize()的次数

在嵩山版中修订了HashMap关于1024个元素扩容的次数修改为:resize()方法总共会调用 8 次。

那么我们来看一下到底是哪8次?

再次重申一下本篇文章主要是讲嵩山版对这个扩容次数的修改,涉及相关的源码已在Java不得不知道的八股文之哈希表 - 掘金 (juejin.cn))中写过,为了避免重复啰嗦,有水文之嫌,所以本文不会再次分析会直接给出对应的结论为已知条件进行分析。如想知道更多细节和版本差异,请移步之前的文章。

以下是我们可以从源码中得出的结论:

  1. HashMap没有初始化容量时默认负载因子为0.75,在第一次put()时会调用resize()进行初始化,容量为16。(JDK1.8)
  2. HashMap中的阈值threshold为capacity * load factor容量*负载因子。例如容量为16,那么阈值threshold就为16*0.75=12。
  3. 当HashMap进行put()的时候,元素个数大于等于当前阈值的时候size>=threshold,会进行resize()操作。容量和阈值都会乘以2。例如初始容量为16,那么当我们put第12个元素的时候,就会进行resize()操作。

基于以上已知的结论,那么我们就能很容易的知道,在JDK1.8中没有给HashMap初始化容量时,存储1024个元素调用resize()的次数和时机了。(默认阈值为0.75)

resize次数 容量 阈值 调用时机 最大存储元素个数
第一次 16 12 存放第1个元素时 12
第二次 32 24 存放第个24元素 24
第三次 64 48 存放第个48元素 48
第四次 128 96 存放第个96元素 96
第五次 256 192 存放第个196元素 196
第六次 512 384 存放第个384元素 384
第七次 1024 768 存放第个768元素 768
第八次 2048 1536 存放第个1536元素 1536

所以在第7次进行resize()后,HashMap的capacity为1024了,但是当我们存放第768个元素时,size>=threshold就会进行第8次扩容,此时才能真正的存放下1024个元素。

所以在JDK1.8中没有给HashMap初始化容量时,存储1024个元素,会调用resize()8次。

其实resize()操作也是十分消耗性能的,我们存储1024个元素的时候没有设置初始值就会调用8次。如果我们给其设置了2048容量,那么我们可以避免了。所以阿里巴巴同时建议我们给HashMap初始化时给定容量,具体规则和细节可以看为什么阿里巴巴建议HashMap初始化时需要指定容量大小? - 掘金 (juejin.cn)

总结

此番修正主要是每个人对「扩容」定义存在了分歧,在JDK1.8中如果没有给HashMap设置初始容量,那么在第一次put()操作的时候会进行resize()。而有的人认为这算一次扩容,有的人认为这不是一次扩容,这只是HashMap容量的初始化。

所以存储1024的元素时:

  • 前者的人认为扩容次数为8次。
  • 后者的人认为扩容次数为7次。

孤尽老师说对此分歧,希望用没有「二义性」的语言来表示,所以「扩容次数」修正为「resize次数」。

相关推荐
ifanatic22 分钟前
[面试]-golang基础面试题总结
面试·职场和发展·golang
儿时可乖了28 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol29 分钟前
java基础概念37:正则表达式2-爬虫
java
Iced_Sheep38 分钟前
干掉 if else 之策略模式
后端·设计模式
xmh-sxh-13141 小时前
jdk各个版本介绍
java
XINGTECODE1 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露