浅谈 HashMap(三):探秘为什么容量和扩容时必须为 2 的幂次方、如何解决 hash 冲突
书接上文:浅谈 HashMap(二):put 插入方法源码分析
本文基于 JDK1.8
是不是经常被面试官问到:为什么HashMap的容量必须为2的幂次方呢?
希望此文可以帮助你找到答案。
要回答上面的问题前,我们还得从其它方面谈起👇
众所周知,HashMap 的底层是数组 + 链表,先根据 key 计算 hash 值,根据 hash 值确定在数组中的位置。
在看源码前,我们不妨猜想一下大量的 key 是如何散列到数组中(此处就用 HashMap 的默认大小16)的,当数组大小为 16 时,数组下标的范围为0 ~ 15,我们通常所想到的实现方式就是根据 key 得到一个0 ~ 15范围内的值,而且要尽量不同(即散列分布)。比如可以用Random.nextInt(16)
方法可以得到这样一个范围的值,那么有没有更加高效的方式呢?我们去源码一探究竟👇
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);
... ...
}
从源码可知,使用了 &运算 来得到一个符合数组下标的值,那么为什么要这样用呢?因为与运算是二进制运算,故而高效。暂且不论公式中的 hash 是如何来的,我们看看这个与运算发生了什么👇
与运算通俗的说就是截取了某个值(即hash值)的二进制后四位,为什么是 4 位呢?我们发现二进制 1111 正好就是十进制的 15,是不是恍然大悟。
我们再想想既然要根据某个值进行与运算,而且还要让每个 key 都不太一样,是不是突然想到了什么?对了,就是Object
类的hashCode
方法,这个方法是个native
方法,返回一个唯一的int
值。int
类型的值占 4 个字节共 32 位,如果只截取后 4 位进行与运算,那么在大量数据面前是不是很容易发生多个 key 散列到同一个数组位置的情况呢?我们去源码看看 hash 值到底是怎么计算出来的👇
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
从源码可知,先调用Object
类的hashCode
方法得到 key 的哈希值并赋值给局部变量 h,h 再和将 h 右移 16 位后的值进行^
(异或运算)。前面的计算再简单来说,就是高16位与低16位进行^
(异或运算)。从而确保每个 key 的 hash 值的后 4 位尽量唯一(但是还是很容易发生 hash 冲突/碰撞,毕竟只有 4 位嘛)。
重点来了👇
从上文知道了如何计算 hash 值并尽量让其唯一,那么当 HashMap 扩容时,如何也能和上文一样高效呢?是不是也可以使用 &运算 呢?那么怎么使用呢?
此刻是不是感觉到了什么,但又模模糊糊。且听我道来,11111 对应的数组大小是 1111 对应的数组大小的 2 倍,那么在扩容时将原容量扩容到 2 倍时,是不是正合我意呢。此时我们再回头看看文章最开始的那个问题,为什么初始容量是 2 的幂次方,并且扩容是要扩容到原容量的 2 倍,是不是恍然大悟了。
作者:超级鲨鱼辣椒
转载请注明原文链接:https://www.cnblogs.com/jinzlblog/p/15119373.html