java基础-25-jdk1.8HashMap解决哈希碰撞

分类: java |
1.
无论是哪个版本的jdk,在扩容时数组大小必须是n=power(2,k),初始大小为16,原因是效率:我们需要对一个对象的hash值判断其所在数组的下标时,直观上需要进行求余操作,而这里就可以简化为位运算hash & (n-1),即求hash值的低位。
例如下面第一个数是某hashCode,而第二个数就是默认数组大小16-1.
http://s13/mw690/002i2qf8zy7ojxs0Ibicc&690
2.
官方native方法中,String对象的hashCode计算如下:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
这里选择了31作为基底,为什么呢?
正如1中所述,HashMap的数组大小是2的k次方,如果我们选择一个整数16作为基底,而此时Bucket数组的大小是16,那么此时哈希(求余)的结果就会很糟糕:
(x*16 + y) % 16 = (x*16) % 16 + y % 16 = 0 + y % 16 = y % 16
也就是说只有最不重要的低位决定了bucket的下标。所以基底的选择应当是素数,同时31又是一个特殊的素数,它是2的5次方减1,可以利用位运算得到,在计算hashCode时,31可以通过移位运算来提高计算效率:31 * i == (i << 5) – I 。
3.
在jdk1.8 HashMap在获取hash值时会作如下处理,而不是直接对bucket数量n求余找到对应的数组下标:
static final int hash(Object key) {
}
在jdk1.7的HashMap在获取hash值时的处理更为复杂:
staticint
hash(int
h)
{
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h
^=
(h
>>>
20)
^
(h
>>>
12);
return
h
^
(h
>>>
7)
^
(h
>>>
4);
}
这种对hashCode的运算函数我们称为扰动函数。
虽然hashCode本身的int类型,数据范围在[-2^32, 2^32-1]内,但内存中不可能存在这么大的数组来表示Bucket,所以才会有1中的解释,默认数组大小仅为16。
http://s8/mw690/002i2qf8zy7ojxszcrl17&690
4.
在上古版本中,HashTable的初始size是质数:11,通过模运算寻找数组下标,Since JDK 1.3, integer division and remainder calculation has become really slow,而移位运算速度比模运算快5倍以上;
HashMap的数组大小是2的次方,从而可以通过位运算来替代模运算提高效率,但这就对hash本身提出了要求,需要随机性更强的hashCode,通过扰动函数来增加随机性,从而解决非质数引起的分布不随机,这就可能从一定程度上抵消了位运算带来的高效,真是个trade off选择。
参考:
[1] https://www.zhihu.com/question/20733617
[2] /questions/3613102/why-use-a-prime-number-in-hashcode
[3] /questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier/299748#299748
[4] /questions/9413966/why-initialcapacity-of-hashtable-is-11-while-the-default-initial-capacity-in-has