首先,我们都知道HashMap在底层设置容量时,会专门设置为2的幂次方。具体原因不作展开,经典八股,可以直接网上搜一下 O(∩_∩)O
我们今天要讨论的是,它是对容量统设为2的幂次方的实现原理,以及其中的一些细节。
函数tableSizeFor()
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;
}
首先,我们举一个最经典的例子————7
7的二进制原码:0111
ps:我们计算机中对数据的是以补码的形式计算的,7作为正数,三码合一。
- 首先,我们对7进行-1得到6,即0110
n |= n >>> 1;展开语法糖,即n = n | n >>> 1- 代入,
n = 0110 | 0011 - 我们可以看到,
0110最高位上的1移到了下一位上,包括其后面的数,也同样进行的后移。而1后面的数我们无法保证其为1或是0,但我们可以保证最高位的1一定移到了下一位上。 - 所以,当我们按位或时,原先1的下一位,一定是和移位后的1进行或运算,结果也必然是1
- 最后将结果返回给n时,那么n此时二进制下,最高位的1后面也必然是1
- 依次类推,我们对1后面的每一位上的数都变为1,最终得到一个全为1的二进制数,作为n
当然,这个函数做了部分优化,我们在每次的或运算后,能保证为1的数也相对应的增加,那么第一次我们只能保证一个位置上的1,那么第二次就变成2个,那么第三次就用这两个1对下两位也进行或运算,就能保证有4个...... 为什么有种快速幂的既视感
在最终返回结果时,我们可以看到n + 1 也就是对全1的二进制数加一,把它变为2的幂次方。就此结束。
所以,我们可以对该实现做个简单的概括:
以传参容量为例,获得大于等于它的最小2的幂次方
以上为基本实现原理,下面我们讨论其中的细节
大家有没有想过,如果传入的容量是个负数,会是什么情况?
我们可以继续举个例子跟着代码走,如果是个负数,那么它同样以补码的形式参与位运算,而此时,符号为1,这就要涉及到位运算的有无符号问题。
我们可以看到,代码中使用的是>>>无符号右移,那么为什么不用>>右移呢?这两者有什么区别吗?
- 在数为正数时,补码中符号位为0,那么无论是带符号的右移,还是无符号右移无脑补0,效果是一致的。
- 在数为负数时,补码中符号位为1,那么两者就有区别了。
- 用>>时仍使用原符号位1填充符号位,那么结果仍为负数
- 而>>>会无脑用0填充符号位,原先的1作为数运算,所以结果会变成一个很大的正数。
但在该代码中,这真的有影响吗?
比如n |= n >>> 1; ,即便n >>> 1变成了巨大的正数,可原先的n仍是负数,在原先的n对其或运算后,符号位上1 | 0 又被改回来成了1,所以最终仍然是个负数,且与>>>的结果也一致。
所以,从结果上看,无论正负,使用>>>或>>在该场景下并无区别。
那么,设计者使用>>>而不是>>的原因是什么?
个人观点:只是作为单纯的操作实现,而不是在意对数值的符号保持。因为容量本身就不该被传负值。^_^
如大佬有不同观点,欢迎讨论~