jdk1.8 HashMap put时确定元素位置的方法与扩容拷贝元素确定位置的方式冲突?
2 个回答
主要在于理解hashmap如何做扩容,大致的效果如下:
图1 扩容的大致效果
如果node1下面没有其他节点,那么直接用"hash&tab.length-1"来定位node1的新位置即可。如果node1下有节点,那么需要遍历这个链表,逐个确定节点的新位置。
红框2中的一段代码,主要是为了将图1中node1~node4进行迁移。代码中的变量loHead/loTail/hiHead/hiTail,分对应了node2,node1,node4,node3。之所以命名lo和hi,就是将新table分为了两部分,原先大小的部分(lo)和新扩容大小的部分(hi)。
代码"e.hash&oldCap==0"是用来判断,当前结点是放在lo中还是hi中。
更新 20160428============
为什么代码"e.hash&oldCap==0"可以确定当前结点是放在lo中还是hi中?
由于capacity始终是2的整数次幂,因此取模可以用二进制与来计算
1.
oldIndex = hash & (oldCap - 1)
= hash % oldCap
==> hash = m * oldCap + oldIndex
2.
newIndex = hash & (newCap - 1)
= hash & (oldCap * 2 - 1)
= hash % (oldCap * 2)
==> hash = n * oldCap * 2 + newIndex
==> n * oldCap * 2 + newIndex = m * oldCap + oldIndex
==> newIndex - oldIndex = oldCap * (m - 2 * n)
3.
m = hash / oldCap
n = hash / newCap = hash (2 * oldCap)
==> m >= (2 * n)
==> newIndex - oldIndex = 0 or oldCap
3.1 若 newIndex - oldIndex = 0
(newIndex = oldIndex) < oldCap
则扩容后,e.hash & oldCap = 0,元素位置不变,仍放在原先的索引位置上
3.2 若newIndex - oldIndex = oldCap
则扩容后,e.hash & oldCap = oldIndex + oldCap,元素需要放到新扩容出的部分里
我试着给你从三方面解答下:
第一 &在JAVA中是按位与; 我们底层数据在计算机中都是用二进制存储的, 而非我们习惯的10进制; &的作用呢就是当比对的两位都是1的时候, 结果才为1, 其他都是0;
比如 2 & 6; 计算机会转换成: 0010 & 0110; 他的结果就是 0010, 也就是2, 这就是&的作用;
第二 我们来看HashMap的扩容方法, 这里首先需要知道HashMap每次扩容是多少, 这需要看HashMap::tableSizeFor ,
( |的作用是只有 0 | 0时才为0, 其余结果均为1 );
tableSizeFor()是获取扩容后hash槽的长度的函数, 采用了一个位或来获取新的hash槽长度;
以27为例,
0001 1100
0000 1111 (>>>1)
|:0001 1111
0000 0011 (>>>2)
|:0001 1111
0000 0000 (>>>4)
|:0001 1111 (到这步其实值已经固定了)
0000 0000 (>>>8)
|:0001 1111
0000 0000 (>>>16)
|:0001 1111
从上可看出tableSizeFor()得到的一定是大于等于本身的值, 且不会大于2^32次方; 大于等于本身是因为初始化值如果小于16, 那么依然会以16作为初始hash槽的大小;
HashMap扩容方法的代码比较长, 我就不贴了, 接下来主要说明下HashMap扩容时, 怎么去移动的旧有节点;
HashMap(1.8)采用了一种很巧妙的方式来判断: (e.hash & oldCap) == 0 这段代码是判断节点是处于当前hash槽还是下一个hash槽的关键;那么为什么(e.hash & oldCap) == 0 可以用来判断是否需要移动呢?
用16的初始长度来举例:
old:
10: 0000 1010
15: 0000 1111
&: 0000 1010
new:
10: 0000 1010
31: 0001 1111 (新增的1 刚好是原hash槽长度 1 0000d的二进制结果为16)
&: 0001 1010
从上面的示例可以很轻易的看出, 两次indexFor()的差别只是新增一个1, 而这个1刚好是oldCap, 那么只需要判断原key的hash这个位上是否为1: 若是1, 则需要移动至oldCap + i的槽位, 若为0, 则不需要移动;
到这里已经解答了你问题二的疑惑; 我们再来看HashMap::put;
第三 tab.length-1&hash 完全等价于 hash&tab.length-1, 这就好比 2 == 1 和 1 == 2 结果都为false一样;
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
这是JDK1.7中计算插入节点位于哪个hash槽的方法, 正因为这个方法太过于简单, 1.8就直接放弃这个方法, 将计算放到代码中了;
所以你标出来的红色部分, e.next == null 说明当前节点是第一个节点, 是链表, 直接重新计算下hash槽的位置就好了, 至于为什么不用再判断新槽点会不会有值, 上面已经解答了;
希望我的回答能解决你的疑惑;