浅谈 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 是如何来的,我们看看这个与运算发生了什么👇

image

与运算通俗的说就是截取了某个值(即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 的哈希值并赋值给局部变量 hh 再和将 h 右移 16 位后的值进行^(异或运算)。前面的计算再简单来说,就是高16位与低16位进行^(异或运算)。从而确保每个 keyhash 值的后 4 位尽量唯一(但是还是很容易发生 hash 冲突/碰撞,毕竟只有 4 位嘛)。

重点来了👇

从上文知道了如何计算 hash 值并尽量让其唯一,那么当 HashMap 扩容时,如何也能和上文一样高效呢?是不是也可以使用 &运算 呢?那么怎么使用呢?

image

此刻是不是感觉到了什么,但又模模糊糊。且听我道来,11111 对应的数组大小是 1111 对应的数组大小的 2 倍,那么在扩容时将原容量扩容到 2 倍时,是不是正合我意呢。此时我们再回头看看文章最开始的那个问题,为什么初始容量是 2 的幂次方,并且扩容是要扩容到原容量的 2 倍,是不是恍然大悟了。

posted @ 2021-08-09 16:40  超级鲨鱼辣椒  阅读(347)  评论(0编辑  收藏  举报