jdk1.8 HashMap put时确定元素位置的方法与扩容拷贝元素确定位置的方式冲突?

如题:jdk1.8中 HashMap 在put元素时使用 tab.length-1&hash 确定元素放入tab[]的位置 但是在扩容复制元素时 却使…
关注者
16
被浏览
3,252

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槽的位置就好了, 至于为什么不用再判断新槽点会不会有值, 上面已经解答了;

希望我的回答能解决你的疑惑;