HashMap 原理分析
HashMap 的原理在我们使用的过程中整体流程还是比较简单的,无非就是放数据和取数据,放数据时通过key的hashcode找到value的索引位置,然后放入数据或者取出数据。那么在这些操作中HashMap做了哪些设计呢?(这里以jdk1.8说明)
1.HashMap结构
如图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
源码如下:
到此我们通过key找到数组的下标了。
5.put流程
put 流程如图:
如图在put时会先判断数组是否为NULL,然后就是前面讲到的获取key节点的映射位置,这里面会设计到散列哈希,和去模算法,获取到映射位置后就分五种情况了。
- 该映射位置为NULL,直接存放数据,类型为Node
- 该映射位置存在数据,且类型为TreeNode,并且遍历树不存在该数据,将数据添加到树中
- 该映射位置存在数据,且类型为TreeNode,并且遍历树存在该数据,将树中数据更新
- 该映射位置存在数据,且类型为Node,并且遍历链表不存在该数据,将数据添加到链表末尾
- 该映射位置存在数据,且类型为Node,并且遍历链表存在该数据,将链表中的数据更新
6.扩容机制
HashMap 中当table中的数据的数量比例达负载因子0.75(默认)时,HashMap开始扩容,因为当table中的数据的数量到达容量*负载因子时,则hash碰撞的概率就会大大增加,此时put 和get的效率就会降低,如果负载因子过大则对之后的扩容影响的数据也越多。如果小于0.75就降低了其空间利用率。0.75平衡了时间和空间的成本。
扩容需要做两件事情:
1.计算新的table的大小,在原基础上乘以2,原容量左移1位
2.遍历所有数据,并将数据重新映射并拷贝到新的位置(由于table容量是2的幂次方,所以所有的数据要么在原来的映射位置要么在原来的映射位置+原table容量大小的位置)
数据拷贝有4种情况
- 数据为NULL,不处理
- 数据为单节点,直接存放到新的table映射位置
- 数据为树节点,拆分树重新映射
- 数据为链表节点,拆分为高链和低链.低链基于当前索引直接存放新的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的幂次方的原因之一)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/5318.html