1.首先HashMap的数据结构
一维数组 + 链表/红黑树(当链表长度大于8则转换为红黑树);
2.那么问题来了:怎么确定数组的索引下标?
(当put一个key-val时, 应该放在数组的哪个位置?)
这个地方就要求尽量达到"均匀散布", why?(均匀散布性越低, ->就会使得链表越长/红黑树越高)
为什么链表越短越好, ->因为链表查找元素只能循环遍历, 数组才能索引快速访问;
=>至此, 目的一致, 以"均匀散布"为目标!!!
->那么往固定长度容器里面放入各种不同元素,怎么才能保证"不超出容器长度"且"公平散布"呢??
->然后HashMap采用了取模(hash(key) % length),即 hash(key) / length = 商......"余数"
(注: 取模公式: a % b = a ÷ b 的"余数")
(这里"取模"肯定能保证不超出容器长度, 但是保证"公平散布"难道是因为hash值比较公平的值??? >存疑-期待大佬指点)
结论:"取模"运算的目的:
1.取到key的hash值(int);
2.用hash值除以数组的长度;
3.得到的"余数"就是元素在数组中的"均匀散布"索引位置(数组下标)
注: 此"余数"肯定不可能超过被除数(数组长度)
3.另"取模"运算是算术运算, 性能低下
所以为了效率 ->应"算术运算"转换为二进制的位运算
因公式: x / 2^n = x >>> n; // 可自行验证(10进制也是如此)
(即: 当被除数为2^n次方时, 除法运算可转换为右移运算 (乘法则左移))
且 "x" 右移出去的部分就是"余数"(即: "x"右移出去的n位 == "余数" == 取模结果);
原因: 二进制逢2进1; (十进制也是同样的, 1234 / 10^2 = 1.34(右移2位))
结论: 取模运算(取余数) == 右移运算中被移出去的部分(前提: 除数必须是2^n)
4. 那现在怎么取"余数"呢??
(以上"右移"运算得到的是"商", 不是"余数")
从第3步可得:
取模: x % (2^n) == x>>> n 的右移出去的部分
=> x % (2^n) == x的二进制的后n位
怎么取一个数的 "后n位" 呢???
很显然: x & (前面全是0,后面n个1) == x的后n位 (因为: "&"运算规则: 都为1->1; 否->0)
又 2^n - 1 刚好满足这个条件(前面全是0, 后面跟n个1)(因为二进制, 同理十进制应是10^n)
原因: 2^n == 00010000"(前面一个1,后面跟n个0) => (2^n-1) == 00001111 (前面全是0,后面跟n个1)
推导过程如下:
( 往map中写的key的hash值->以下简称hash, map的数组长度length= 2^n)
HashMap数组下标 == hash % 2^n (注释: 取模运算)
== hash / 2^n的余数 == hash >>> n 这个右移运算的"移出去的部分"
== hash & (2^n-1) (注释: 取hash的后n位)
== hash & (length - 1)
这不最终结果就出来了; 和源码写的一模一样的 ;) 源码如下
(另: 很显然能看到, "余数"的计算仅和"hash的后n位有关", 根本没用到hash的前几位, => 所以jdk1.8后, 在object的hash方法计算的hash值又做了一次高低位的亦或运算, 目的是:混合高低位,使得数组下标计算时利用到整个hash值,而不是只有后n位参与), 源码如下:
所以HashMap必须保证数组的长度length == 2^n; 就算你设置了初始化容量也不会生效, 他会自动往上扩容至2的幂;
"欢乐的时光很快就过去了, 又到时候讲拜拜";)