先上源代码
private static int roundUpToPowerOf2(int number) {
int rounded = number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY
: (rounded = Integer.highestOneBit(number)) != 0 ?
(Integer.bitCount(number) > 1) ? rounded << 1 : rounded
: 1;
return rounded ;
}
过于抽象,换成通俗一点的
if (number >= MAXIMUM_CAPACITY){
rounded = MAXIMUM_CAPACITY;
}else{
if ((rounded = Integer.highestOneBit(number)) != 0){
if (Integer.bitCount(number) > 1){
rounded = rounded << 1;
}else {
rounded = rounded;
}
}else {
rounded = 1;
}
}
当输入的 number 超过最大值,直接掰回来,将大小设置为最大值;如果 number 小于最大值,则进行一个对 number 取最高位为 1,其余位为 0 的骚操作。直接看 Integer 内的 highestOneBit 方法
public static int highestOneBit(int i) {
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
这个方法依旧很抽象,首先明白几个知识点
1、补码: 对于正数而言,补码即为原码; 例如:17 = 【10001】
2、 >>: 将i转换成二进制后将该数往右平移;
3、 |: 按位取或,输入有1时则为1,否则为0; 例如:10001 | 00100 = 10101
int i = 17;
Integer.toBinaryString(i); // 10001
Integer.toBinaryString(i >> 1); // 01000
Integer.toBinaryString(i | i >> 1);// 11001
了解了基本规则后便可发现,该方法先对 i 往右平移 1 位,同时与原值进行按位取或,如同上边代码中的注释,保证最高位的下一位也为 1,从而得到一个最高位开始连续两个数均为 1 的值
然后进行下一步,由于最高位为连续的两个 1,因此对该值进行往右平移 2 位,并按位取或,此时可以发现,已经获得了一个最高位连续为 1 的值
Integer.toBinaryString(i); // 11001
Integer.toBinaryString(i >> 2); // 00110
Integer.toBinaryString(i | i>> 1); // 11111
当经过了几轮这样的右移操作,就能得到一个从最高位开始全部为 1 的二进制数,对其进行无符号右移一位( >>> 忽略符号位右移,同时空缺位补0)
Integer.toBinaryString(i); // 11111
Integer.toBinaryString(i >>> 2); // 01111
Integer.toBinaryString(i - i >>> 2); // 10000
至此,得到了一个小于或等于 i 的 2^N 次方的数,通过观察发现,当输入的 number 为 0 时后,highestOneBit 方法会返回一个 0,此时 roundUpToPowerOf2 将会返回 1,
当输入的number满足了【 highestOneBit(number) ! = 0 】的要求后,使用bitCount方法对number的二进制形式的 1 的数量进行判断,当 bitCount 返回的值等于 1 时,代表 number 即为 2^N 次方的数,否则 number > rounder,通过左移一位使得改值翻倍,得到一个足够容下 number 的大小并且大小为 2^N 次方的空间。
JAVA8里面这段代码改的更精简了
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;
}
首先对cap - 1,然后进行一系列位移运算,得到的是一个大于等于cap - 1并且最高位连续为1的值,通过+1便得到一个2^N,同时能保证这个2^N大于或等于cap。
知道如何生成一个 2^N 之后,就可以研究一下 HashMap 为何必须使用 2^N 作为 HashMap 的容量大小了。
jdk1.7中的 HashMap 是由数组加链表组成,每次 put 的时候都要对 key 值做一个极其烧脑的 hash 操作,得到一个 hash 值
// 整一个足够均匀的hash值
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 获得下标
static int indexFor(int hash, int length) {
return hash & (length-1);
}
大部分情况下这个 hash 值基本不会重复,hash值需要放入对应的数组下标,hash值为一个int,但并不能保证生成的hash值大小一定小于我们计算得到的大小(2^N),正常思维下我们会把hash对数组长度取模运算,但这属实太慢了,远不如位移运算快,改几个电位比取模快多了,于是决定将取模改为位移运算,但问题又来了,采用按位与运算时,为什么一定要2的N次方呢,因为这样可以保证运算出来的值肯定在 2^N -1 内,
例如 当数组长度分别为8 和 7时,hash值为2和3
length - 1
0000 0011 3 0000 0010 2
& 0000 0111 7 & 0000 0111 7
= 0000 0011 3 = 0000 0010 2
-------------------------------------------------------
0000 0011 3 0000 0010 2
& 0000 0110 6 & 0000 0110 6
= 0000 0010 2 = 0000 0010 2
会发现当length为 2^N 时,length - 1 进行与运算时hash值总能落在 length 内(得到的值肯定不大于 0111 ),同时对比 length 为 7 时的与运算发现,当 length 为 7 时( 0110 )全部下标为 xxx1 的空间都不可使用,因为对 0110 进行与运算时无法得到 xxx1 的结果。
以上均为个人理解,如有错误请指出。