在 jdk8 的 HashMap 中做了很大的改动,使得Map的性能大幅提升,其中有这么一段代码:
/**
* Returns a power of two size for the given target capacity.
*/
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 + 1;
}
这个方法是用于找到大于或等于输入 cap 的最小的2的幂,比如当我输入5时,大于或等于5的最小的2的整数次幂为8(23),则该方法返回8。
输入(整数形式) | 输入(二进制形式) | 返回(整数形式) | 返回(二进制形式) | 返回值(2的幂形式) |
---|---|---|---|---|
1 | 0001 | 1 | 0001 | 20 |
2 | 0010 | 2 | 0010 | 21 |
3 | 0011 | 4 | 0100 | 22 |
4 | 0100 | 4 | 0100 | 22 |
5 | 0101 | 8 | 1000 | 23 |
6 | 0110 | 8 | 1000 | 23 |
7 | 0111 | 8 | 1000 | 23 |
8 | 1000 | 8 | 1000 | 23 |
通过这个方法可以实现提前设置一个较为稳定的容量,从而避免频繁扩容导致的性能下降。
这个方法的实现原理比较有趣,位运算占据了方法的主体。为了分析整个运算,我这里简化了一下,只保留了有效操作:
>>>
无符号位右移
>>>
是无符号右移运算,无符号表示运算不考虑符号位。对于int
类型的数而言,符号位为最高位。当我想要表示一个正数2时,最高位符号位为0,对应的结果为:
当我想要表示一个负数-2
,则最高位符号位为1,其余位要用补码表示:
无符号右移表示最高位符号位也参与位移运算
,如果对 -2 使用无符号右移的话,由于符号位的"1"被移动到了其他位上,此时我们会得到一个非常大的正整数。
但这明显与我们预期不符,因此无符号位右移通常运用在正整数上。算术上的效果展现为除以2数次幂。
n |= n >>> 1
分析
我们回到代码上来,n |= n >>> 1
不仅仅表示无符号右移,而且同时按位或了n本身,最终又将计算得到的结果赋值给了n。
直接看的效果应该不明所以,不知道这样是什么意思。但是如果我们一位一位来看的话应该会清楚一些,由于有按位右移逻辑,我们保留最低位为0。这样的话我们可以带入n=2,执行一下看看会发生什么效果:
图片上表现出来的效果可能不太清楚,但宏观上表述的话就是最高位的1填补了低它一位的位置。也就是20010
中的1填充了低它一位的位置,最终变成了30011
。
再进一步分析的话会发现最高位低一位的位置是0或1对这个运算结果都没什么影响,因为最终都会被高位的1填充:
位移或运算
理解了这里的代码后,我们不难发现我们只需要研究最左边为1的哪一位对它身后的小兄弟做了什么即可:
其中最后一步n|n>>>16
:
简单来说既是不论我们输入一个什么样的正数,在 return
前我们最终都会得到一个2n-1的数。接下来再加1就能后得到我们预期的结果了。
为什么要 cap-1
?
回到开头我们来研究一下 int n = cap - 1
起到了什么样的作用。此时如果我们输入一个15,程序执行到该行会得到一个14:
好像没什么作用。因为不论你怎么减,只要不影响到最高位的1,最终结果都不变的。
但实际这里处理的是输入参数为 2 的整数次幂的值。如当输入值为16时:
这里当我们去掉减一操作
后再试试:
二进制的展现效果如下:
也就是说最开始的减一操作
当我们输入了2的整数次幂时,返回其本身,而不是将扩充一倍。