《HashMap系列》第一集:基础字段、构造函数、基础算法

本文详细解读了HashMap的内部结构、构造原理、put()方法、resize()机制、hash算法和容量计算,以及为何树化标准设为8。涉及字段、构造方法、树化规则和优化技巧,适合深入理解数据结构与Java哈希表实现。
摘要由CSDN通过智能技术生成

传送门

《HashMap系列》第一集:基础字段、构造函数、基础算法

《HashMap系列》第二集:put()方法解析

《HashMap系列》第三集:resize()扩容机制

《HashMap系列》第四集:get()方法解析

《HashMap系列》第五集:remove方法解析

一、HashMap结构示意图

个人总结注意事项:

1.JDK7时,HashMap采用数组+链表的结构, 链表插入为头插法, JDK8的时候, HashMap采用数组+链表+红黑树,链表插入元素为尾插法

2.HashMap是懒加载,调用完构造方法以后,并未创建对应的数组,只有在第一次存数据的时候调用resize()方法才会创建数组

3.构造方法有4个,

    无参构造方法的容量和加载因子使用默认值

    有参构造方法创建时使用阈值字段来暂时存储初始类容量大小,然后在第一次存数据的时候,调用resize()方法,使用阈值字段来创建数组,然后用阈值字段乘以加载因子重新赋值该字段

4.put()方法存数据时候,当链表长度大于默认阈值8的时候,会进入treeifyBin()方法,该方法里面会判断数组长度是否小于MIN_TREEIFY_CAPACITY=64,如果小于则进行扩容,否则则转换成红黑树

5.resize()扩容的时候,

    源码前半部分判断主要分3个方面:(1) 非第一次扩容,最大值判断之后, 容量变为2倍 (2) 第一次扩容,调用有参构造方法,生成新容量值 (3) 第一次扩容,调用无参构造方法,生成新容量值 

    源码后半部分判断主要是将链表中的元素存入到新数组中

6.HashMap中单独定义了hash()方法,主要就是保证key的hash值更加散列

7.tableSizeFor()方法是返回大于输入参数且最近的2的整数次幂的数

8.当HashMap中的键值对的个数大于threshold值则进行扩容

9.虽然JDK8提供了红黑树的结构来保证数据,但是我们使用的时候通过 阈值8 和 最小树行化阈值64 还要扩容机制 可以看出来,我们平常使用到红黑树的情况还是很少,因为如果真的存储这么多元素,那么就可以使用其它的存储方式了,但是如果有的人真的较真,JDK官方还是提供了红黑树的结构

二、字段

1)静态常量字段

2)成员变量

三、构造方法

HashMap提供了4个构造方法

四、为什么HashMap底层树化标准的元素个数是8?

大概意思就是:如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,注释中给我们展示了1-8长度的具体命中概率,当长度为8的时候,概率概率仅为0.00000006,这么小的概率,HashMap的红黑树转换几乎不会发生,因为我们日常使用不会存储那么多的数据,你会存上千万个数据到HashMap中吗?

五、hash算法

hash算法是计算其hash值,正好object类中提供了hashCode()方法,通过使用hashCode()方法,我们可以获得key对应的hash值。那我们直接拿这里的hash值与(长度-1)去做&运算不就得到对应的下标了吗?其实不然,因为table的长度都是2的幂,因此index仅与hash值的低n位有关,hash值的高位都被与操作置为0了,那么hash的高位的存在就等于毫无意义,这显然不是公平的算法。要是高16位也参与运算,会让得到的下标更加散列。

这段代码是什么意思呢?将key的hashcode与key右移16位进行异或得到的才是真正的hash值?

为了更好的理解,我们来看一个例子,假设table.length=16。

由上图可以发现,在计算下标时,只有hash值的低四位参与了下标的计算。key的hashcode与key右移16位进行异或得到的hash值,结合了前16位和后16位,所以相对来说是非常公平的。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。

六、容量算法: tableSizeFor()方法

tableSizeFor的功能(不考虑大于最大容量的情况)是返回大于输入参数且最近的2的整数次幂的数。比如10,则返回16。该算法源码如下:

下面关于tableSizeFor的图形化分析参考另外博主的文章 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;
}

第一行很简单,为什么要-1放在最后说,最后一行是两个三目运算符,其中之一操作是n+1,都很容易理解。关键是中间五步移位加上或运算。

移位的思想

2的整数幂用二进制表示都是最高有效位为1,其余全是0,比如十进制8和32,下图只用了一个字节示意。

2的整数幂形式

对任意十进制数转换为2的整数幂,结果是这个数本身的最高有效位的前一位变成1,最高有效位以及其后的位都变为0

核心思想是,先将最高有效位以及其后的位都变为1,最后再+1,就进位到前一位变成1,其后所有的满2变0。所以关键是如何将最高有效位后面都变为1

还是用图来示意。这里将十进制的25转换为32。

25转为32

作者的做法是先移位,再或运算。

右移一位,再或运算,就有两位变为1;

右移两位,再或运算,就有四位变为1,,,

最后右移16位再或运算,保证32位的int类型整数最高有效位之后的位都能变为1.

初始值

选取任意int类型数字,下图x表示不确定0或者1.
任意数字x

我们目的是将所有的x变为1,如下图

所有x变为1

最后+1,就能进位得到2的整数幂。

最终结果形式

我们要做的就是不断通过右移+或运算来达到目的。

右移一位+或运算

右移1位或

可以看出,右移一位再或运算,有两位变成了1。

右移二位+或运算

右移2位或

右移两位再或运算,有四位变成了1。

右移四位+或运算

右移4位或

右移四位再或运算,有八位变成了1。

右移八位+或运算

右移8位或

右移八位再或运算,有十六位变成了1。

右移十六位+或运算

右移16位或

右移十六位再或运算,注意这里不是三十二位全变,而是最高位后面的全变1。

结果+1

结果+1

可以看出,不管x是多少,我们都能将其转换为1。而且分别经过1,2,4,8,16次转换,不管这个int类型值多大,我们都会将其转换,只是值较小时,可能多做几次无意义操作。

 

初始容量-1

之所以在开始移位前先将容量-1,是为了避免给定容量已经是8,16这样2的幂时,不减一直接移位会导致得到的结果比预期大。比如预期16得到应该是16,直接移位的话会得到32。在上图中就是所有x本身已经是0的情况下,不减1得到的结果变大了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为人师表好少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值