static final int hash(Object key){
int h;
return (key ==null)?0:(h=key.hashCode())^(h>>>16);
}
在hash函数中,先调用了key的hashCode()方法,这将返回一个int类型的哈希值,比如说字符串的hashCode。
public int hashCode()(
int h = hash;
if (h == 0 && value.length >0){
char val[] = value;
for (int i =0;i<value.length;i++{
h =31 *h+ val[i];
}
hash = h;
}
return h;
}
int的范围是-2147483648-2147483647,加起来大概40亿上下的浮动。
只要key的hashCode方法设计的比较合理,一般是很难出现碰撞的。
但问题是,不可能直接搞一个40亿长度的数组啊,那也太铺张浪费了。
我们一般会设置一个较小的数组长度,比如说HashMap的数组初始大小才16,当发现容量不满足的时候再扩容,避免浪费。
那当数组长度比较小的时候,我们就需要设计一种比较巧妙的hash算法,来避免发生哈希冲突,尽可能地让元素均匀地分布在数组当中。
要达到这个目的,HashMap在两方面下足了功夫,第一个就是数组的长度必须是2的整数次幂,这样可以保证hash&(n-1)的结果能均匀地分布在数组中。
其作用就相当hash%n,n为数组的长度,比如说数组长度是16,hash值为20,那么20%16 =4,也就是说20这个元素应该放在数组的第4个位置;hash值为23,那么23%16=7,也就是说23这个元素应该放在数组的第7个位置。
&操作的结果就是哈希值的高位全部归零,只保留n个低位,用来做数组下标访问。
比如说hash&($2^{4} -1$)的结果实际上是取hash的低4位,这四位能表示的取值范围刚好是0000到1111,也就是0到15,正好是数组长度为16的下标范围。
以初始长度16为例,16-1=15
2进制表示是0000 0000 0000 0000 0000 0000 0000 1111.和某个哈希值做&运算,结果就是截取了最低的四位
那问题又来了,那么大一个哈希值,也只取最后4为,不就等于哈希值的高位都丢弃了吗?
比如说1111 1111 111 1 1 1 1 1 1 1 1 1,取最后4为,也就是1111.
比如说11101111101111111111111111111111111,取最后4为,也是11111
不就发生哈希冲突了吗?
这时候hash函数(h=key.hashCode())^(h>>>16)就派上用场了呀.
将哈希值无符号右移16位,意味着哈希值的高16为被移到了低16位的位置。
这样,原始哈希值的高位和低16位就可以参与到最终用于索引计算的低位中。
选择16位是因为它是32位整数的一半,这样处理既考虑了高位的信息,又没有完全忽视低位原本的信息,尝试达到一个平衡的状态。
举个例子(数组长度为16)
第一个数:h1=0001 0010 0011 0100 0101 0110 0111 1000
第二个数:h2=0001 0010 0011 0101 0101 0110 0111 1000
如果没有hash函数,直接取低4为,那么h1和h2的低4位都是1000,也就是两个数都会放在数组的第8个位置。
来看一个hash函数的处理过程。
对于第一个数h1的计算:
原始:0001 0010 0011 0100 0101 0110 0111 1000
右移 :0000 0000 0000 0000 0001 0010 0011 0101
异或--------------------------------------------------------
结果:0001 0010 0011 0101 0100 0100 0100 1101
通过上述计算,我们可以看到h1和h2经过h^(h>>>16)操作后得到了不同的结果。
现在,考虑数组长度为16时(需要最低4位来确定索引)
对于h1的最低4位1100(十进制中为12)
对于h2的最低4位是1101(十进制中为13)
这样,h1和h2就会被分别放在数组的第12个位置和第13个位置上,避免了哈希冲突。