今天工作有点累,就小水一篇吧。
聊一聊HashMap的数组大小为何设计成2的n次方?
先说答案:
- 简化求余操作;
- 减少扩容时元素移动的概率,提高resize操作的性能。
HashMap底层是数组+链表来实现的,插入<K,V>时,首先是计算Key的hash值,然后和数组大小取余,余数是几就把该元素放到数组的哪个位置上。
当数组大小len为2的n次方时,取余操作可简化为:
hash & (len - 1)
比如len=8,其二进制为:
0000 1000
len-1后:
0000 0111
hash & (len - 1)相当于是直接取hash值的后几位,后几位为几就放到哪个桶里。
发生扩容时,len会往左移动1位,比如8变为16:
0000 1000 --> 0001 0000
对应到上面,其实就是从看hash值的后3位,变成了看hash值的后4位
相当于在原来的基础上再往前看1位
如果往前1位为0,则元素不发生移动,若往前1位为1,则需要进行元素移动。
通过这样的设计,可以减少扩容时元素移动的概率,提高resize操作的性能。
那有读者问了,如果我初始化HashMap的时候,手动传入1个非2的n次方的数,那不就破坏上述的设计了吗?
其实是不会的,如果你传入的初始长度不是2的n次方,HashMap会将它强制转化成比它大且离它最近的2的n次方作为初始长度。
即tableSizeFor函数。
static final int tableSizeFor(int cap) {
// 将-1往右移动Integer.numberOfLeadingZeros(cap - 1)位
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
// 如果n<0,则返回1
// 如果n>0,且n >= MAXIMUM_CAPACITY,返回MAXIMUM_CAPACITY
// 否则返回n+1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
比如tableSizeFor(21) = 32。
该函数的关键在于Integer.numberOfLeadingZeros方法。
Integer.numberOfLeadingZeros的目的是获取Integer数的二进制串中从左到右连续"0"的个数。
public static int numberOfLeadingZeros(int i) {
// HD, Count leading 0's
// 如果i等于0,说明有32个0,如果小于0,首位肯定为1,所以前置0的个数为0
if (i <= 0)
return i == 0 ? 32 : 0;
int n = 31;
// 若i大于1 << 16,说明数字的16位之前必定有1,将n-16,i >>>= 16取16位之前的数字继续比较
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
// 若i大于1 << 8,说明数字的8位之前必定有1,将n-8,i >>>= 8取8位之前的数字继续比较
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
// 若i大于1 << 4,说明数字的4位之前必定有1,将n-4,i >>>= 4取4位之前的数字继续比较
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
// 若i大于1 << 2,说明数字的2位之前必定有1,将n-2,i >>>= 2取2位之前的数字继续比较
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
return n - (i >>> 1);
}
以数字21为例:
其二进制为:
0000 0000 0000 0000 0000 0000 0001 0101
很明显,i >= 1 << 4,n-4=31-4=27,而i >>>= 4变为:
0000 0000 0000 0000 0000 0000 0000 0001
27-(1 >>> 1) = 27-0 = 27
所以21的前置0个数为27,跟我们直接数的是一致的。
读者可自行验证其他数字
这个方法巧妙在"二分法"和移位的操作。
重新回到tableSizeFor方法:
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
之所以用-1,是因为-1的二进制所有位均为"1",即:
1111 1111 1111 1111 1111 1111 1111 1111
之所以要cap-1是为了避免cap本来就是2的指数幂
比如21-1=20的二进制为:
0000 0000 0000 0000 0000 0000 0001 0100
Integer.numberOfLeadingZeros(cap - 1)为27
-1 >>> 27为:
0000 0000 0000 0000 0000 0000 0001 1111
最后再加1返回:
0000 0000 0000 0000 0000 0000 0010 0000
即32。
可以看出来,JDK会对任何一个小的地方进行优化,尽最大可能提升性能。
这种反复锤炼过的代码真是美如画,令人沉醉,哈哈哈!