默认初始容量是16. HashMap 的容量必须是2的N次方, HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方, 例如传 9, 容量为16,
JDK8的计算代码:
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;
}
经过5个无符号右移的计算, 得到的值是一个低位全是1的值, 最后返回的时候 +1, 就会得到1个比n 大的 2 的N次方.
开头的 cap - 1 , 这是为了处理 cap 本身就是 2 的N次方的情况.如当cap=16时, 计算结果是32, 而cap-1=15的计算结果是16.
如, cap=30时, 经过5次 >>> 和 |= 运算后, 得到的值为 :
0001 1111
其对应的值为31, 31+1 = 32
JDK11中的代码:
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
Integer.numberOfLeadingZeros 是算出cap-1二进制中前导0的个数, 如cap=14时, 算出前导0个数为28.
负数在计算机中是以补码的形式来表示的, -1的二进制为32个1. 对32个1无符号右移28位后,得到低位4个1的数,其十进制为15, 最后再+1就是16.
0000 0000 0000 0000 0000 0000 0000 1111 -> 15
而使用cap-1去计算前导0个数的原因也跟上面是一样的.
两者性能比较
从性能角度分析,JDK8所做的5次无符号右移运算, 在cap较小时, 可能做了很多次无用功, 如当cap=15时, 5次右移|=的计算结果都是1111, 白白多做了4次运算.
而numberOfLeadingZeros()方法内部采用了二分法的思想, 快速得到了前导0的个数.
下面例子分别调用JDK8和11的tableSizeFor()方法10亿次, 打印出分别的用时.
static final int MAXIMUM_CAPACITY = 1 << 30;
public static void main(String[] args)
{
long start1 = System.currentTimeMillis();
for (int i = 1; i < 1000000000; i++)
{
tableSizeFor8(i);
}
long end1 = System.currentTimeMillis();
System.out.println("JDK8, 计算用时:" + (end1 - start1) + "ms");
long start2 = System.currentTimeMillis();
for (int i = 1; i < 1000000000; i++)
{
tableSizeFor11(i);
}
long end2 = System.currentTimeMillis();
System.out.println("JDK11, 计算用时:" + (end2 - start2) + "ms");
}
static final int tableSizeFor8(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;
}
static final int tableSizeFor11(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
输出结果为:
JDK8, 计算用时:1522ms
JDK11, 计算用时:793ms
可以看到JDK11的算法几乎快了一倍!