为什么 HashMap 的容量大小要设置为2的N次方?

原文链接:https://www.changxuan.top/?p=1208


前两天,我在一位同学提交中看到了下面这样的一行代码,让我很是惊讶。

Map<String, String> temp = new HashMap<>(6);

我给他说,你这样实例化 Map 对象不好用,他不服气。我说小朋友:如果想指定 HashMap 对象的容量得用2的N次方。他说你这也没用。我说,我这个有用,这样才能充分利用分配的内存空间。他非和我试试,我说可以,不过得先一起看看源码。

什么是HashMap?

在弄懂标题的问题之前,首先需要清楚 HashMap 的概念。HashMap 是基于哈希表的 Map 接口的实现,线程不安全,且不保证映射顺序。

HashMap 存储数据依赖的是数组和[链表|红黑树],具体链表和红黑树之间如何转换的细节此文不做详细介绍。而本文开头提到的实例化容量大小指的则是数组的大小。

如何计算元素在数组中所对应的下标?

首先计算元素的哈希值,方法如下:

static final int hash(Object key) {
        int h;
     // h = key.hashCode();
     // h = h ^ (h >>> 16)
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

为什么不直接使用 key.hashCode()的值,我们后面会提到。

计算出来哈希值后,由于数组容量相对来说较小肯定不能直接使用哈希值当作索引值。所以需要使用哈希值对数组长度减一后的值取模。不过在在 HashMap 中可不是直接使用 % 运算符来操作的。为了提高效率,采用的是与运算的方式,代码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
      // n 为数组容量, (n-1) & hash 则是计算索引值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
          ... ...
        }
}

既然清楚了计算元算在数组中所对应下标的方法,那么证明为什么实例化 HashMap 对象的容量要使用2的N次方就简单多了。

假如初始容量为2的3次方数字8,当哈希值与容量大小减一的值进行与运算时可以保证结果比较均匀的分布在数组上。

  10100101 11000100 00100101
& 00000000 00000000 00000111 // 7
----------------------------------
  00000000 00000000 00000101 // 结果可以是[0,7]中的任一数字

如果初始容量为6,那么出现哈希冲突的几率就会增加了。

  10100101 11000100 00100101
& 00000000 00000000 00000101 // 5 
----------------------------------
  00000000 00000000 00000101 // 5
  
  10100101 11000100 00100111
& 00000000 00000000 00000101 // 5 
----------------------------------
  00000000 00000000 00000101 // 5

如果下面的值低位全是1,那么上面的这次哈希冲突则可以避免。那么你想想,假如指定的容量大小为5又会怎么样呢?其实2的N次方数字-1的二进制形式这个特性在好多地方会很好用,可以在小本本记上。

哦,前面说为什么计算出来的散列值需要再让高16位和低十六位做异或运算,主要是让参与与运算的位同时具有高位和低位的特征,来减少哈希碰撞次数。

小朋友,还试不试啦!

上一篇:JAVA强制类型转换原理


下一篇:网络地址运算