哈希函数的目标
均匀分布:使不同的键能均匀地分布在哈希表中,减少哈希冲突,即尽量让不同的键计算出的哈希值分散在哈希表的不同位置,提高存储和查找效率。
高效计算:能够快速地计算出键的哈希值,以保证HashMap的插入、查找和删除等操作具有较好的性能。
初始哈希值计算
首先会调用键对象的 hashCode() 方法,该方法是 Object 类的方法,每个Java对象都有。若类没有重写 hashCode() ,则会根据对象的内存地址等信息生成一个默认哈希值。
很多类如 String 、 Integer 等都重写了 hashCode() 方法以实现更合理的哈希值计算。以 String 类为例,它根据字符串的字符序列计算哈希值,保证相同内容的字符串哈希值相同,不同内容的字符串哈希值尽量不同。
二次哈希(扰动函数)
得到的初始哈希值是⼀个32位的int类型的数值,然后让hashcode的⾼ 16位和低16位进⾏异或操作,使得哈希值的高位和低位都参与运算,进一步打乱哈希值,使其分布更均匀。
异或操作运算如下:
同时,这么设计也是为了降低哈希碰撞的概率。
索引计算
因为 key.hashCode() 函数调用的是 key 键值类型⾃带的哈希函数,返回 int 型散列值。int 值范围 为 -2147483648~2147483647,加起来⼤概 40 亿的映射空间。
只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个 40 亿长度的数组,内 存是放不下的。
假如 HashMap 数组的初始⼤⼩才 16,就需要⽤之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。
源码中模运算就是把散列值和数组长度 - 1 做⼀个 " 与& " 操作,位运算⽐取余 % 运算要快。
这也正好解释了为什么 HashMap 的数组长度要取 2 的整数幂。因为这样(数组长度 - 1)正好相当于⼀个 “低位掩码”。 与 操作的结果就是散列值的⾼位全部归零,只保留低位值,用来做数组下标访问。以初始长度 16 为例,16-1=15。2 进制表⽰是 0000 0000 0000 0000 0000 0000 0000 1111 。和某个散列值做 与 操作如下,结果就是截取了最低的四位值。