hashMap的put操作的底层原理分析

我们对hashmap的操作最频繁的是put和get操作,这次我们就从put操作开始讲起,大致了解hashmap的基本原理。

put方法,当hashmap被创建时,首先不会申请容量,在进行put操作时,会进行第一次扩容,第一次扩容的容量为16,以后扩容将原容量乘2。

1、存放索引的计算:

将元素放入时,首先要计算该元素放入数组的索引。索引的计算如下:

首先获得该元素key的hash值,再将hash值与其值右移16位进行异或操作。其目的为综合高位(稍后详解)。再将得到的异或值与当前容量-1进行&(与运算),就得到了存储的下标位置。

得到存储索引后,就会判断当前数组位置中是否已经有元素在这儿,如果没有就将该元素放入数组索引的这个位置。如果有,在jdk1.7中直接将元素插入链表头部;jdk1.8中,先判断该节点的类型,如果是树节点,那么则进行树节点的操作逻辑,如果是链表节点,则先遍历得到节点的个数,再考虑是否需要树化。其中,树化的前提为:

首先判断该链表中是否已经有8个节点,如果没有则不进行树化,将节点插入链表尾部;如果节点个数已经大于等于8,那么再进行判断,数组的容量如果小于64,则进行扩容,不进行树化,若数组的容量大于等于64,则进行树化。

在遍历时也要判断在此索引位置的所有节点,是否与加入的元素相等,若相等则将之前的value覆盖并返回。都不相等则将元素插入该链表。

2、索引计算的三个问题:

(1)为什么数组的获取hash值时需要与容量-1进行与运算,为什么容量要减1?

hashcode的值为32位,为了便于理解,我们在这里以八位进行讲解。如若key的hash值为107,转换为二进制为  0110 1011,当前数组容量为16 装换为二进制 为 0001 0000

若直接将hash值与容量进行与运算,得到结果如下

0110 1011     hash值

0001 0000    容量

0000 0000    结果

由于容量为2的n次方,所以容量的二进制只有一位为1,那么得到的结果也只能与hash值的一位有关,无法达到散列的效果。

我们继续观察,在这里如果将hash值的第五位(从右往左数)的值换成1,那么得到的结果为 0001 0000 也就是容量的值。那么此时获得的下标就已经越界了!

因此,将容量减1,在这里hash值就与   0000 1111 做与操作,hash值的更多位也参与了索引的计算,不仅能达到更好的散列,还能解决数组下标越界的问题。

我们接着思考,当数组容量较小时,高位的值都为0,那么hash值的高位不也不能参与索引的计算了?这也是我们接下来要讨论的的第二个问题。

 

(2)为什么在获取hash值时,要将原有hash值再与其右移16位,进行异或,得到新的hash值(二次hash)呢?

正如我们前面所讲,当数组容量较小时,容量的高位都为0,那么hash值的高位也就无法参与索引位置的计算。如果我们将hash值右移16位(因为hashcode值为32位,右移16位也就是将高位移至低位),在这里演示我们只使用了8位,因此我们将hashcode右移4位。仍以前面的例子进行分析,原有的hash值为107 二进制为0110 1011,数组容量为16 二进制为 0001 0000。

我们首先要获得新的hash值:将key的hashcode1右移4位(原本是右移16位)得到hashcode2,在将hashcode1 与 hashcode2 进行异或运算(相同为0,不同为1),得到新的hashcode值,此过程称为二次hash:
0110 1011     hashcode1

0000 0110     hashcode2

0110 1101     新hashcode

当我们再次计算索引时,就将新的hashcode值,与(容量-1)进行与运算。那么此时经过低位与高位的综合,高位也就参与到了索引的计算,再次提高了索引的散列效果。计算索引的值为

0110 1101     新hashcode

0000 1111      容量-1

0000 1101     结果

放入的下标就为13

其中,二次hash的前提是,数组的容量是2的n次幂。

 

(3)数组的容量为何是2的n次幂?

a.在我们计算索引时,有两种方法可以实现,第一种为通过模运算(hashcode值%数组容量),第二种也就是上述所讲的使用与运算。那么为什么选择的第二种呢?因为在计算中,数的运算都是需要转换为二进制来进行计算的,取余运算最终也会转换为二进制的运算。而与运算能够提高计算索引的效率(由于笔者水平有限,建议大家可以去看看其他关于这个问题的详细教程)因此我们选择与运算。

b.在jdk1.8中还利用了这一设计,进行扩容时重新计算索引做出了优化,当hashcode值与原容量进行&(与操作),注意这儿是直接和容量进行与操作,而不是计算下标时的容量-1。得到的值如果为0,则元素不会移动,若不为0,则元素的位置为:旧位置+原容量。我们依然使用前面的例子来进行分析,下一次扩容时容量会增加为64。hash值与原容量进行与操作为:

0110 1101       得到的hash值

0001 0000       原容量

0000 0000       结果

按照我们前面所说,应该留在原位,接下来进行验证,将hash值和新容量-1进行与运算

0110 1101     hash值

0001 1111      新容量-1

0000 1101      结果

可知,下标的位置仍然为13

若另外一个hash值为0001 1101,可知他与原容量(16)-1进行与运算,仍然是13,所以在没扩容时,他们两是在一个索引位置的。判断他与原容量进行与运算,是否等于0

0001 1101

0001 0000

0001 0000

可知,不为0,那么他的新位置会在哪儿呢

0001 1101

0001 1111

0001 1101

通过计算他的索引位置为 13 + 2^4 = 29

通过验证,我们的公式是没有问题的,但判断这一规律的关键条件在哪儿呢?
我们发现,其关键位置就在原容量的二进制为1的位置上,如果hash值对应的位置为0,那么与原容量进行与运算也会为0(因为容量的其余位置都为0),由于扩容计算索引时(旧容量-1)的该位置由0变为1成为与(新容量-1)进行与运算,但hash值的这个位置为0,因此计算结果是不变的。如果hash值的此位置不为零,那么计算的结果会比以前的多2^n,也就是原数组的容量。

 

如有错误,欢迎各位大佬指正!!!

 

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值