美文网首页互联网科技Java架构技术进阶
不按套路出牌,HashMap负载因子超过1会怎样?

不按套路出牌,HashMap负载因子超过1会怎样?

作者: 代码小当家 | 来源:发表于2020-04-07 16:06 被阅读0次

推荐阅读:

47天时间,洒热血复习,我成功“挤进”了字节跳动(附Java面试题+学习笔记+算法刷题)​zhuanlan.zhihu.com

图标 Java学习笔记、源码、面试题库、思维脑图领取,点击此文档​shimo.im 图标

前言

提到HashMap,文章可谓是不计其数,“详解HashMap”啊,“HashMap源码解析”啊,“细说HashMap啊”,“胡说HashMap”啊....

image

再提到内部实现,稍有常识的同学估计张口就能说出个一二三来,哈希桶啊,链表加数组啊,1.8 之后链表过长会转为红黑树啊等等。但今天笔者想要带来的是,除了探究一下标题上的HashMap负载因子超过1会发生什么? 还要看看我们耳熟能详的红黑树,它到底在多大数量级会出现? 让我们一起测试一下!

回顾

测试之前,先回顾一下 Hashmap 都有哪些重要的元素? 首当其冲重要的就是三个成员变量 capacity、loadFactor、threshold,其中 capacity 和 loadFactor 是可以从构造函数中传入的,通俗的来捋一波:

capacity : HashMap 中桶的数量, 总容量默认值是 16 。需要注意的是初始容量必须是 2 的幂次方。 关于这个属性,还让笔者想起昨天在脉脉上看的小段子,一个员工吐槽:"在老板的代码里发现了new HashMap<> (3)这段代码"。刚刚笔者也说到初始容量必须是 2 的幂次方,但是写成 3,也不会报错,强大的 hashmap 都帮我们考虑好了,内部用了 tableSizeFor 这个方法,帮我们转换了一下:

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
intput  3 |  4  |  5  |   8    ......   
output  4 |  4  |  8  |   8

capacity 就是传入的 3,hashmap 拿到 3 并不会直接使用,经过 tableSizeFor 运算后会得到 2 的幂次方 4,所有你这里传 3 还是传 4,hashmap 都会优化为 4,脉脉上吐槽的员工估计是想嘲笑一下老板连基础知识都不扎实吧,我觉得老板是 Capacity 用不用减一记混了可能比较大吧,也不影响使用。言归正传..

loadFactor:负载因子,默认 0.75。也就是threshold是根据 capacity * loadfactor 算出来的,HashMap 会根据 threshold 的数值,来决定什么时候调整空间大小。 这个参数有点意思。首先默认 0.75,这个如果不深究倒是好解释,取了 0.5~1.0 的中间值嘛,但是如果我不按套路出牌设置为 2 ,HashMap会怎样呢?

image

动手看看效果

测试

上代码:

HashMap<String, String> testMap = new HashMap<>(2, 2);
testMap.put("test1", "val");
testMap.put("test2", "val");
testMap.put("test3", "val");

初始 capacity 给了 2,loadFactor 也给了 2,那么 threshold 即为 4,容量是 2 ,到 4 才扩容,但是我 put 了三个元素进去容量已经超了,并且还没有触发扩容,会发生什么? 将断点打入 HashMap 的 putVal 方法,观察执行效果:

  1. 第一个元素 test1 进来,进入了两个分支:
if ((tab = table)i == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

首先桶是空的,进行初始化的 resize,根据构造函数的参数,初始化的容量只有 2,紧着这进入第二个分支,进行与运算计算出数组的索引,一看 “嗯这地方没人,我 new 一个放这”。

  1. 第二个元素进来,差不多同上,但是因为数组已经不是 null 了,所以没有进入第一个分支。
  2. 第三个元素进来,因为总长度只有 2,test3 根据 hash 方法计算后的值是 110250867,所以(2 - 1)& 110250867 的结果也只能是 1,key 为 test3 的元素就被追加到索引为 1 的元素后方。

也就是说,如果我们构造函数传入的 loadFactor 大于 1,HashMap 并不会报错而是像 hash 冲突一样,追加到计算出的索引后方,提前形成了链表,查找元素的时间复杂度也就高了起来。

关于转换为红黑树

紧接着来看看第二个疑问,到底多大数据量,会出现红黑树? 首先 HashMap 有个重要的参数,就是TREEIFY_THRESHOLD 默认是 8,变量名很规范,一眼就能看出是干什么的,意如其名就是转为树的阈值,源码中的首次判断是否需要转为树,就是直接用的它:

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

上面这段代码也是 putVal 中的一部分,通常负责为 hash 冲突的节点追加链表元素,-1 for 1st这句是源码里自带的,因为 binCount 是从 0 开始嘛,所以阈值减一,解释了一下。 就像代码表述的,到了阈值将会调用treeifyBin方法,那么 treeifyBin 就会直接进行红黑树转换吗? NO! 我们看看方法实现的前三行:

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();

方法先校验了 hash 桶的数量,要是小于MIN_TREEIFY_CAPACITY的话,重新调整大小即可,默认值是 64,也就是说并不只是有TREEIFY_THRESHOLD一个参数的限制。 知道了从哪里转为红黑树,想要进行我们的测试,最简单暴力的方式就是将 treeifyBin 方法第二个分支打个断点。 我们将之前测试的构造函数删掉,采用默认的构造函数,写下测试代码:

HashMap<Integer, String> testMap = new HashMap<>();
int max = 1_000_00000;
for (int i = 0; i < max; i++) {
    testMap.put(RandomUtil.randomInt(Integer.MAX_VALUE), "val");
}

简单暴力,随机一千万个 key,当第一次进入我们打的断点时,也就代表第一颗红黑树形成。点开 Idea 的 Evaluate 窗口,输入 this.size 就能看到当前 hashmap 的长度,我记录了十次第一个红黑树形成时的 size大小:

1\. 2696228
2\. 5685561
3\. 559996
4\. 2806127
5\. 没出现
6\. 6156770
7\. 768019
8\. 5577574
9\. 739868
10\. 8870967

十次下来大概平均三百万数量级左右,会出现第一颗红黑树,当然这不是一次非常严谨的测试,统计次数,测试数据的随机分布性,都有可能影响结果,但是这个结果也能体现两点:

  1. 红黑树没有想象中那么容易出现
  2. hashmap 很强大,将数值分布的很均匀。

结尾

两点测试就到这里,我们可以思考一下,既然数量级这么大才会出现红黑树,HashMap 还犯得着优化吗?笔者推测了一下,个人观点如下: HashMap 我们可以看到类 doc 的@since 注解是1.2,早在 jdk1.2 的时候 HashMap 就出现了,jdk1.2 是什么时间发布的呢,搜索可知是 1998 年!约 20 年前的电脑内存等配置和现在已经是今非昔比,现在的内存条件,已经可以操作比当年更大更长的对象了,当然也别忘了 JVM 也要进行调优。

image

所以,综合我的观点来看,HashMap 作为最常用的数据结构之一,进行优化的意义是非常大的!

作者:Vt
链接:https://juejin.im/post/5dcb53a8e51d45225371ad30
来源:掘金

相关文章

  • 不按套路出牌,HashMap负载因子超过1会怎样?

    推荐阅读: 47天时间,洒热血复习,我成功“挤进”了字节跳动(附Java面试题+学习笔记+算法刷题)​zhuanl...

  • HashMap的负载因子为什么是0.75

    HashMap的负载因子是指,达到容器的最大容量*负载因子,容器就扩容。那么负载因子为什么不设置成1呢?这样空间利...

  • 随笔微思|如何看待不按套路出牌?

    #每天一点心理学# 你按套路出牌了吗? 如果有人说:你怎么不按套路出牌?你会怎么回答?会不会在想:如果我按套路出牌...

  • 不按套路出牌+1

    FTN价格一路下探。 如果按照投资界众多资深专家总结出的“买涨不买跌”的投资策略,在此刻,可绝对不是买进FTN的好...

  • HashMap以及其子类关键性总结

    HashMap 1.7中的HashMap 负载因子: 给定默认容量为16 负载因子为0.75 Map在使用过程中...

  • ConcurrentHashMap 1.7和1.8原理

    前言 以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决...

  • 深入并发包 ConcurrentHashMap( 上 )

    前言 以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决...

  • JDK1.7和JDK1.8中ConcurrentHashmap

    以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的...

  • java-01-ConcurrentHashMap

    前言 以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决...

  • 不按套路出牌

    老师列了一些必读书目,昊说,我看完了《逃家小兔》。 那你想离家出走吗? 不想! 为什么(满心期待)? 因为我没有钱...

网友评论

    本文标题:不按套路出牌,HashMap负载因子超过1会怎样?

    本文链接:https://www.haomeiwen.com/subject/xtynphtx.html