Java 与 JVM · 教程

HashMap的Hash函数到底有什么意义

jdk1.8 HashMap中hash值计算源码

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

// 其中的key.hashCode调用的是底层的native方法。
public native int hashCode();
  • Q: 为什么不直接使用key的hashCode值呢?

如果直接使用key的hashCode()函数的时候,将hash值作下标访问Map的数组的话,由于2进制的取值范围是[-2^31,2^31),加起来大于有40亿的映射空间。只要hash函数散列的比较松散,那么久很难出现碰撞。当然,40亿的数据,内存也是放不下的。所以不能直接拿key的hashCode。

  • Q: 为什么要进行 h = key.hashCode()) ^ (h >>> 16)这个运算呢?

这个问题就要从计算出的hash值的作用上来切入了,hashMap计算这个hash值就是为了什么? 增删改查的时候确定元素在数组中的位置。那么hashMap是怎么确定位置的呢? 我们知道HashMap中的数组是tab字段。根据tab找到,在final HashMap.Node<K,V> getNode(int hash, Object key)方法中,有这么tab[(n - 1) & hash]使用。 n是数组的长度. 这里n-1相当于一个低位掩码,和hash进行与操作的结果就是散列值的高位全部归零,保留低位,用来做下标访问. 这么看,就算我们的hash散列分布再松散,冲突也会很严重。以默认长度16为例。

  11111111  11111111 11110000 11101010  # native方法 HashCode()
& 00000000  00000000 00000000 00001111  # 默认初始长度 15 = 16 -1
  -------------------------------------
  00000000  00000000 00000000 00001010  # 进行&操作,计算出的下标为10

我们看下使用扰动函数计算后的结果:

# 扰动函数计算过程:
   h=hashCode():  11111111  11111111  11110000  11101010  # native方法 HashCode()
   -------------------------------------------------------------------------------
              h:  11111111  11111111  11110000  11101010  # native方法 HashCode()计算出的值。
   ^     h>>>16:  00000000  00000000  11111111  11111111  # hashCode左移16位=>高16位变低位
   -----------------------------------------------------------------------------------
hash=h^(h>>>16):  11111111  11111111  00001111  00010101  # 扰动函数计算出的值


# 与掩码做&运算=>计算数组下标
hash=h^(h>>>16):  11111111  11111111  00001111  00010101  # 扰动函数计算出的值
            &  :  00000000  00000000  00000000  00001111  # 低位掩码=> hashMap中数组的长度
    -----------------------------------------------------------------------------------
                             ....                  00101  # =>计算出的下标为5.

Peter Lawley的一篇专栏文章《An introduction to optimising a hashing strategy》里的的一个实验

hashMap扰动函数的作用.png

结果显示,当HashMap数组长度为512的时候,也就是用掩码取低9位的时候,在没有扰动函数的情况下,发生了103次碰撞,接近30%。而在使用了扰动函数之后只有92次碰撞。碰撞减少了将近10%。看来扰动函数确实还是有功效的。

JDK7中的HashMap计算hash的方式

参考文章

HashMap中的hash函数

最后

期望与你一起遇见更好的自己

期望与你一起遇见更好的自己

配套资料 · 免费领取
本文属于「JAVA」资料包范围:JDK 源码笔记合订版。HashMap、并发、Stream 等系列合订。
在公众号「方家小白」回复关键词 JAVA 即可领取。全文无需关注即可阅读。
JAVA