HashMap原理分析

HashMap 原理分析


HashMap 的原理在我们使用的过程中整体流程还是比较简单的,无非就是放数据和取数据,放数据时通过key的hashcode找到value的索引位置,然后放入数据或者取出数据。那么在这些操作中HashMap做了哪些设计呢?(这里以jdk1.8说明)

1.HashMap结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ios6Wp9x-1645003303208)(C:%5CUsers%5C75412%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20220216143633797.png)]

如图HashMap内部维护着一个数组,数组的每个节点只有两种数据类型要么是Node代表链表,要么是TreeNode代表红黑树,下面我们来看存入数据时(put数据时)HashMap是怎么定位数组下标的,这里就涉及到HashMap的散列哈希算法和索引映射算法了。

2.散列哈希

一般情况下得数组下标,我们会想到通过对key获取hashcode,然后对数组长度去模获得数组下标。这种方式也是HahMap实现方式,但是HashMap不是简单通过hash函数获取hashcode,因为hashcode的散列效果不是很好,HashMap实现了一种高效的方式能使hashcode均匀的散列到数组中,这里我们称为散列哈希。(这里需要注意的是通过hash函数得到的hashcode是int类型,32位。)

散列哈希算法:hashcode ^ (hsahcode >> 16)

hashcode:    				1000110010100110    1101010100100110

hsahcode  >> 16:            0000000000000000    1000110010100110

结果:						1000110010100110    0101100110000000

源码如下:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

通过散列哈希算法我们得到更加散列的hashcode值,接下来就是去模了,一般我们会想到hashcode%table长度去模,但是这个效率不够高,HashMap也实现了一种高效去模算法。该去摸算法要求数组长度必须是2的幂次方才行。下面我们来看一下数组table的长度是怎么计算的。

3.容量table的计算

前面获取了更加散列的hashcode值,接下来去模就能获取key的下标了,但是HashMap的去模算法要求数组长度必须是2的幂次方,所以就必须在初始化或者扩容时计算数组长度。其实数组长度必须是2的幂次方不仅仅能提高去模的效率,还能在扩容是搬移数据的数量减少一半,这个我们在扩容的地方详细介绍。

扩容时好解决,只需要将原来的长度扩大到原来2倍就可以了,1 << 原来的table的长度,原来的table的长度左移1位就可以了。

那么在初始化时呢,默认情况下HashMap的长度为16,如果初始化时传入了指定容量呢,HashMap 实现了容量table的计算算法。

	int n = cap - 1; // cap代表传入的容量值以n = 1000110010100110    1101010100100110为例
    n |= n >>> 1;     
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
	n = n + 1;

第一行是为了防止cap刚好是2的幂次方

n:				1000110010100110    1101010100100110

n >>> 1:	 	0100011001010011    0110101010010011

n |= n >>> 1:	1100111010110111    1111111110110111   //将前2位变为1

n |= n >>> 2;   1111xxxxxxxxxxxx    xxxxxxxxxxxxxxxx   //将前4位变为1

n |= n >>> 4;   1111xxxxxxxxxxxx    xxxxxxxxxxxxxxxx   //将前8位变为1

n |= n >>> 8;   1111xxxxxxxxxxxx    xxxxxxxxxxxxxxxx   //将前16位变为1

n |= n >>> 16;  1111xxxxxxxxxxxx    xxxxxxxxxxxxxxxx   //将前32位变为1

n = n + 1;           此时n+1变成2的幂次方

所以该算法是将int类型的n转为二进制后第一个1后面的所有位置都置位1的算法(该算法只针对32位的int类型)

类似的如果是64位的话,就是:

int n = cap - 1; // cap代表传入的容量值
n |= n >>> 1;     
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
n |= n >>> 32;
n = n + 1;

源码如下:

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

4.索引映射

前面我们可以发现HashMap保证了数组的容量一定是2的幂次方后,接下来就是HashMap中的去模算法了

去模算法:hashcode & (tableLength-1)

以hashcode  = 1000110010100110    1101010100100110   tableLength = 64为例

hashcode:	  1000110010100110    1101010100100110

tableLength :                               111111

结果:   	  0000000000000000    0000000000100110		(32+4+2=38)

该去模算法的效率高于 hashcode%tableLength

源码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PWd2uii5-1645003303210)(C:%5CUsers%5C75412%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20220216172106103.png)]

到此我们通过key找到数组的下标了。

5.put流程

put 流程如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8xdo3RIr-1645003303211)(C:%5CUsers%5C75412%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20220216162211393.png)]

如图在put时会先判断数组是否为NULL,然后就是前面讲到的获取key节点的映射位置,这里面会设计到散列哈希,和去模算法,获取到映射位置后就分五种情况了。

  1. 该映射位置为NULL,直接存放数据,类型为Node
  2. 该映射位置存在数据,且类型为TreeNode,并且遍历树不存在该数据,将数据添加到树中
  3. 该映射位置存在数据,且类型为TreeNode,并且遍历树存在该数据,将树中数据更新
  4. 该映射位置存在数据,且类型为Node,并且遍历链表不存在该数据,将数据添加到链表末尾
  5. 该映射位置存在数据,且类型为Node,并且遍历链表存在该数据,将链表中的数据更新

6.扩容机制

HashMap 中当table中的数据的数量比例达负载因子0.75(默认)时,HashMap开始扩容,因为当table中的数据的数量到达容量*负载因子时,则hash碰撞的概率就会大大增加,此时put 和get的效率就会降低,如果负载因子过大则对之后的扩容影响的数据也越多。如果小于0.75就降低了其空间利用率。0.75平衡了时间和空间的成本。

扩容需要做两件事情:

1.计算新的table的大小,在原基础上乘以2,原容量左移1位

2.遍历所有数据,并将数据重新映射并拷贝到新的位置(由于table容量是2的幂次方,所以所有的数据要么在原来的映射位置要么在原来的映射位置+原table容量大小的位置)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gAkuuZN3-1645003303212)(C:%5CUsers%5C75412%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20220216165616186.png)]

数据拷贝有4种情况

  1. 数据为NULL,不处理
  2. 数据为单节点,直接存放到新的table映射位置
  3. 数据为树节点,拆分树重新映射
  4. 数据为链表节点,拆分为高链和低链.低链基于当前索引直接存放新的table,高链存放在新的table的(原索引+原容量)的位置

高链和低链逻辑,以64---->128为例

扩容前:

以hashcode1  = 1000110010100110    1101010100100110   tableLength = 64为例

hashcode1:	   1000110010100110    1101010100100110

tableLength :                                111111

结果:   	   0000000000000000    0000000000100110		(32+4+2=38)


以hashcode2  = 1000110010100110    1101010101100110   tableLength = 64为例

hashcode2:	   1000110010100110    1101010101100110

tableLength :                                111111

结果:   	   0000000000000000    0000000000100110		(32+4+2=38)



扩容后:

以hashcode1  = 1000110010100110    1101010100100110   tableLength = 128为例

hashcode1:	   1000110010100110    1101010100100110

tableLength :                               1111111

结果:   	   0000000000000000    0000000000100110		(32+4+2=38)   ------------->低链

以hashcode2  = 1000110010100110    1101010101100110   tableLength = 64为例

hashcode2:	   1000110010100110    1101010101100110

tableLength :                               1111111

结果:   	   0000000000000000    0000000001100110		(64+32+4+2=64+38)---------->高链

可以看出当tableLength *2时,二进制位数多了1位,当多出来的高位与hashcode对应的位置&运算会得到0或者1,当为0时此时结果与原来没有扩容时一致,当为1时,改位的值是等于2^高位位置结果是等于原table大小的。所以扩容后的值只会有两种情况要么和原来一样要么就是原来的值+原table容量(这也是table必须为2的幂次方的原因之一)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

haohao_ding

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

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

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

打赏作者

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

抵扣说明:

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

余额充值