看过HashMap源码的人可能都用印象,就是hashMap的哈希表长度可以由自己指定也可以不指定使用默认长度,但是如果在了解或者发现tableSizeFor方法的话,你就会知道此方法会改变我们的输入长度 (如果我们输入15,他会改为16),那么他为什么要修改我们设置的长度,以及修改后有什么作用?带着这个疑问我们往下看;
1. HashMap 的长度为什么需要是2的幂次方
为了能让hashMap存取高效,尽量减少碰撞,也就是要尽量把数据分配均匀。
Hash值的取值范围-2147483648到2147483647,总共有40+亿个映射空间,只要哈希函数映射的比较均匀,一般应用很难出现碰撞,但是内存肯定不能一次加载这么长的数组,所以这个散列值是不能拿来直接用的,我们只能创建合理长度的数组作为哈希表,在插入数据之前做取模运算,得到的余数就是将要存放的数据在哈希表中对应的下标。在HashMap中这个下标的取值算法是:(n - 1) & hash
n是哈希表的长度。
取模运算中如果除数是2的幂次方则等价于 其与除数减一的&
操作,就是:hash % length == hash & (length - 1)
采用二进制位操作 &
相对于 %
能够提高运算效率,这也就解释了为啥HashMap的长度需要为2的幂次方
2. HashMap怎么实现的
先看下JDK8的源码:
/**
* 方法保证了HashMap的哈希表长度总位2的幂次方
* 返回大于输入参数且最近的2的整数次幂的数
*/
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;
}
我也是看到这个方法后才决定写这篇博文来记录一下我的感想,我已经不知道多少次被源码作者所折服,我甚至觉得这才是代码应该的样子;
结果分析
n第一次右移一位时,相当于将最高位的1右移一位,再和原来的n取或,就将最高位和次高位都变成1,也就是两个1;
第二次右移两位时,将最高的两个1向右移了两位,取或后得到四个1;
依次类推,右移16位再取或就能得到32个1;
你是不是不知道要32个1干嘛?自己体会如下:
2的幂次方 | 二进制表示 | 十进制标识 |
2^0 | 1 | (1-1)+1 |
2^1 | 10 | (2-1)+1 |
2^2 | 100 | (4-1)+1 |
2^3 | 1000 | (8-1)+1 |
2^4 | 10000 | (16-1)+1 |
2^5 | 100000 | (32-1)+1 |
二进制数字如果都是1的话,那么他加一后就是首位为1其他位都是0,这个数字肯定是2的幂次方, 2^n == 1<<n
举例说明:
10的二进制是1010,减1就是1001
第一次右移取或: 1001 | 0100 = 1101 ;
第二次右移取或: 1101 | 0011 = 1111 ;
第三次右移取或: 1111 | 0000 = 1111 ;
第四次第五次同理
最后得到 n = 1111 ,返回值是 n+1 = 2 ^ 4 = 16 ;
自己实验一把
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
System.out.println(Integer.toBinaryString(n));
n |= n >>> 2;
System.out.println(Integer.toBinaryString(n));
n |= n >>> 4;
System.out.println(Integer.toBinaryString(n));
n |= n >>> 8;
System.out.println(Integer.toBinaryString(n));
n |= n >>> 16;
System.out.println(Integer.toBinaryString(n));
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
public static void main(String[] args) {
int a = 65538;
System.out.println(Integer.toBinaryString(a));
int i = tableSizeFor(a);
System.out.println(i);
}
执行结果:
10000000000000010
11000000000000001
11110000000000001
11111111000000001
11111111111111111
11111111111111111
131072