HashMap实现原理之扩容两倍机制
前言
上一期【HashMap实现原理·二】中简单说了一下hashmap中取元素,相信大家对hashmap的put和get应该有所了解,那么本章讲讲hashmap扩容的知识。
回顾
首先回顾一下hashmap的结构图
扩容前后hashmap中数组对比
扩容后的数组下标图
扩容源码中的部分,可在【HashMap实现原理·一】中的扩容方法(resize)中查阅
源码分析
/**
* hashmap中计算key的hash值的方法
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们可以在前面第一篇文章中看到,hashmap的put方法中有下面这一段,重点是:(n - 1) & hash
(n - 1) & hash 有什么特殊的作用?为什么hashmap容量是2的幂数并且扩容也是2倍增长?
在hashmap的put方法中,(n - 1) & hash 用来计算key的hash值对应在数组中的位置,n是数组的长度,hash是key 的hash值,我们来进行如下运算:
假设:n = 16,对应的二进制是 10000,那么n-1的二进制是 1111
例1:hash = 6,对应的二进制 110,(n - 1) & hash = 6
例2:hash = 7,对应的二进制 111,(n - 1) & hash = 7
例3:hash = 8,对应的二进制 1000,(n - 1) & hash = 8
例4:hash = 9,对应的二进制 1001,(n - 1) & hash = 9
例5:hash = 10,对应的二进制 1010,(n - 1) & hash = 10
例6:hash = 11,对应的二进制 1011,(n - 1) & hash = 11
例7:hash = 12,对应的二进制 1100,(n - 1) & hash = 12
假设:n = 10,对应的二进制是 1010,那么n-1的二进制是 1001
例1:hash = 6,对应的二进制 110,(n - 1) & hash = 0
例2:hash = 7,对应的二进制 111,(n - 1) & hash = 1
例3:hash = 8,对应的二进制 1000,(n - 1) & hash = 8
例4:hash = 9,对应的二进制 1001,(n - 1) & hash = 9
例5:hash = 10,对应的二进制 1010,(n - 1) & hash = 8
例6:hash = 11,对应的二进制 1011,(n - 1) & hash = 9
例7:hash = 12,对应的二进制 1100,(n - 1) & hash = 8
可以看到,取了7个例子,当n为16时,key在数组中的下标分布还是比较均匀的,而当n为10时,发生了比较明显的hash碰撞,我们知道hashmap是通过链表或红黑树存储数据的,当发生严重的hash碰撞,使得元素分布不均匀,导致数组的某几个位置上元素剧增,容易发生链表树化的过程即链表转红黑树。
当hashmap扩容为2倍后,需要重新计算元素的位置,扩容后hash碰撞也会减少,最坏的情况,hash碰撞不变,也就是不需要移动元素位置。当存在需要移动元素的位置时,也只需要在当前位置加上扩容前的容量即可。这点在hashmap扩容方法(resize) 中有所体现,如上图(扩容前后hashmap中数组对比)
举个例子:
假设扩容前:n = 16,对应的二进制是 10000,那么n-1的二进制是 1111
例1:hash = 14,对应的二进制 1110,(n - 1) & hash = 14
例2:hash = 15,对应的二进制 1111,(n - 1) & hash = 15
例3:hash = 16,对应的二进制 10000,(n - 1) & hash = 0
例4:hash = 17,对应的二进制 10001,(n - 1) & hash = 1
例5:hash = 18,对应的二进制 10010,(n - 1) & hash = 2
例6:hash = 19,对应的二进制 10011,(n - 1) & hash = 3
例7:hash = 20,对应的二进制 10100,(n - 1) & hash = 4
假设扩容后:n = 32,对应的二进制是 100000,那么n-1的二进制是 11111
例1:hash = 14,对应的二进制 1110,(n - 1) & hash = 14
例2:hash = 15,对应的二进制 1111,(n - 1) & hash = 15
例3:hash = 16,对应的二进制 10000,(n - 1) & hash = 16
例4:hash = 17,对应的二进制 10001,(n - 1) & hash = 17
例5:hash = 18,对应的二进制 10010,(n - 1) & hash = 18
例6:hash = 19,对应的二进制 10011,(n - 1) & hash = 19
例7:hash = 20,对应的二进制 10100,(n - 1) & hash = 20
&(与)运算分析:与运算的特性,相同取1,不同取0,从这个特性不难得出,任何正整数与若干个1(这里指二进制)与的结果等于本身(两个与数的位数相等)。
tip:当n为2的幂数,且仅当hash大于0时,(n - 1) & hash的结果等于hash%n(至于为什么hashmap用前者,可能是考虑到hash为负值的情况,也可能是计算机中位运算效率快,当然作者的想法也不一定对,欢迎大家一起探讨)
从上述可以得出以下结论:
- (n - 1) & hash用来计算key的hash值对应在数组中的位置
- 容量为2的幂数可以使元素分布更均匀,减少hash碰撞的概率,提升性能
- 由2可以知道,容量为2的幂数的好处,同时扩容为2倍还有一个好处,若需要移动元素位置,则在扩容前的元素下标加上扩容前的容量即得到扩容后的元素下标
结束语
本次文章简单介绍了一下hashmap的扩容特性、为什么初始容量是2的幂数和扩容两倍的好处。如内容有误,欢迎各路大神指正。