我们上一篇文章主要介绍了HashMap的底层数据结构、构造方法、重要的属性,在上一篇我们遗留了一个问题,那就是为什么HashMap的大小必须是2的整数次幂,这一篇文章,我们从源码的角度来解决这个问题。首先我们回顾一下上一篇文章的重点内容
1)HashMap的底层数据结构是数组+链表+红黑树,我将要有一篇文章重点讲解HashMap的链表、红黑树。
2)底层数组的容量大小必须是2的整数次幂。这篇文章重点讲解
3)扩容相关的两个重要属性loadFactor(加载因子)和threshold(阀门),其中threshold=底层数组容量大小*loadFactor;
4)HashMap构造函数中并没有初始化底层数组的大小,底层数组的大小是第一次调用put时初始化的。
讲解这个知识点之前,我们要知道.
1)调用HashMap无参构造函数HashMap(),底层数组的大小为1<<4,也就是16,2的4次幂
2)调用HashMap有参构造函数HashMap(int initialCapacity) 通过tabSizeFor计算得到大于或等于initialCapacity最接近的2的整数次幂。
例如:用户HashMap(8).通过tabSizeFor方法得到8,HashMap(7)通过tabSizeFor方法得到8,HashMap(9)通过tabSizeFor方法得到16.其中tabSizeFor的第一行代码cap-1的作用就为了防止用户给定的就是2的整数次幂,上面8,如果没有cap-1,通过下面的计算获得了16.实际上应该是8,所以知道了cap-1的作用了。
综上所述,不管是默认的,还是我们指定的大小,最终底层数组容器的大小一定为2的整数次幂,这是为什么呢?我们接下来从源码解读分析。
第一步:我们要从put的方法说起,HashMap的put源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
我们首先看hash(key),HashMap为了性能的,利用Hash散列存储的,但是不管hash算法如何的好,都有可能出现hash冲突,HashMap利用key的hash值的高16位与低16位进行异或来降低hash的冲突。源码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
异或的运算规律:两个操作数,相同则为0,不同则为1,举例说明:
1)2^3=1
2)9^17=24
HashMap通过key的hash值的高16位和低16位异或运算,是基于时间、效率多方的考量。
我们在进入putVal()方法
p = tab[i = (n - 1) & hash]
逻辑与的运算规则:两个操作数,全1则1,否则为0
putVal其他的我们暂且不分析,就分析其中上面的代码(n-1)&hash,找出下标,加入我们默认容器16,当put值时,散列的越分散越好,最好的情况就是没有碰到hash冲突,而(n-1)&hash尽可能的会均匀分布,我们上面知道,n一定是2的整数次幂。我们接下来举例说明:
如果我们默认底层数组的大小n=16,计算出的hash值分别1,2,3,4,5,6.......12,通过(n-1)&hash的结果如下:
可以从以上的结果中看得出,通过(n-1)&hash可以均匀的分布。
如果n不是2的整数次幂,n=20,计算出的hash值分别1,2,3,4,5,6.......12 (n-1)&hash
通过的上面的列子可以看出,如果n不是2的整数次幂,(n-1)&hash分布的很不均匀,会导致计算出的下标冲突,形成链表或者红黑树,导致性能下降,有的同学会有两个疑问?
疑问1:为什么要n-1呢?
我们上面证明了n必须是2的整数次幂,如果直接利用n&hash来计算的话,n转换成二进制,只有最高位为1,其余位数都为0,逻辑与运算&规律:全1则1,否则为0,也是为了避免分布不均的情况。
疑问2:获取数组的下标,为什么不用取余运算呢? hash%n
逻辑与运算的性能要高于取模运算,实际上HashMap中的(n-1)&hash的功能和取模运算的功能相同。
通过上面的讲解,我们是不是知道了为什么HashMap的底层容器的大小必须是2的整数次幂呢,现在我们总结一下:
1)HashMap通过key的高16位与key的低16位异或运算key.hashCode()^(key.hashCode>>>16),这样是基于从时间、效率等综合方面考虑的。
2)HashMap的底层容器大小必须是2的整数次幂,因为(n-1)&hash分布的更均匀,而(n-1)&hash是获取底层数组的下标,通过(n-1)&hash,减少冲突。只有是2的整数次幂,(n-1)&hash才能起到很好的作用。
3)(n-1)&hash和取余(%)运算达到同样的效果,但是前者效率远高于后者。
4)彻底理解底层数组容器的大小必须是2的整数次幂的源码(n-1)&hash进行分析的