final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
...
这里提出一个公式:当n为2的指数幂,x%n 等同于x&(n - 1)
由于按位与操作比取模的性能要高,所以为了使用按位与来对hash值取模,HashMap通过tableSizeFor()方法保证了数组长度n为2的指数幂。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap没有直接使用key的hashCode值,而是拿hashCode值与自身右移16位后的结果做异或操作。
原因是为了让key的hashCode的高16位参与取模。
hashCode右移16位后高16位全部为0:
1111 1111 1111 1111 1111 1111 0101 0101 //右移前
0000 0000 0000 0000 1111 1111 1111 1111 //右移后
而这俩个值的异或操作的结果高16位保持不变,低16位就是原来的低16位与高16位的异或结果。
再来看n-1的值,由于n是2的指数幂,所以不难推出n-1的二进制如下:
0000 0000 0000 0000 0000 0000 0000 0001 //1
0000 0000 0000 0000 0000 0000 0000 0011 //3
0000 0000 0000 0000 0000 0000 0000 0111 //7
0000 0000 0000 0000 0000 0000 0000 1111 //15
0000 0000 0000 0000 0000 0000 0001 1111 //31
显然,在数据量不多的情况下,n-1的高16位都是0,而与0做按位与结果还是0,相当于将hash值的高16位忽略掉了。
那么hash值若是存在高16位不同,而低16位完全相同的数时,就会因为n-1的特性导致对俩个完全不同的hash值取模结果却相同,产生大量的hash冲突。例如:
1111 0011 1010 1111 0101 0101 0101 0101 //hash值1
1100 1100 1010 1100 0101 0101 0101 0101 //hash值2
因此对hash算法的优化就是为了让hashCode的高低位都参与取模,降低冲突概率。