HashMap中并没有直接使用KV中K原有的hash值; 在HashMap的put、get操作时也未直接使用K中原有的hash值,而使用了一个hash()方法。让我们一起看一下这个方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这段代码类似作用是为了增加hashcode的随机性
key.hashCode()
的作用是返回键值key
所属类型自带的hashcode
,返回的类型是int
,如果直接拿散列值作为下标访问HashMap
的主数组的话,考虑到int
类型值的范围[-2^31 , 2^31 -1]
,虽然只要hash
表映射比较松散的话,碰撞几率很小,但是映射空间太大,内存放不下,所以先做对数组的长度取模运算,得到的余数才能用来访问数组下标
hashMap源码中模运算逻辑是把散列值和数组长度-1做一个位与
操作,
h & (table.length-1)
这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为数组长度-1
相当于一个低位掩码
。位与
操作的结果就是散列值的高位全部归零,只保留低位值.以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111
。和某散列值做位与
操作如下,结果就是截取了最低的四位值。h & (length - 1)
和 h % length
,它俩是等价不等效的,明显位运算
效率非常高。
01111010 00111100 00100101
& 00000000 00000000 00001111
----------------------------------
00000000 00000000 00000101 //高位全部归零,只保留末四位
but 只取后四位,即使散列值分布再松散,碰撞几率还是很大。更糟糕的是如果散列函数做的比较差吧,分布上成个等差数列啥的,恰好使最后几个低位呈现规律性重复,就比较蛋疼
这时候 “hash”函数作用就出来了
- 右位移16位,正好是32bit的一半,高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来
// (h = key.hashCode()) ^ (h >>> 16) 效果如下 01111010 00111100 00100101 11000101 // 原始哈希码 (key.hashCode()) ^ 00000000 00000000 01111010 00111100 // 原始哈希码的高半区 (key.hashCode() >>> 16) -------------------------------------------------------------------------------------- = 01111010 00111100 01011111 11111001 // 高半区信息完整保留,并加大低半区的随机性
- 设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,少了系统的开销,也不会造成因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
- 根据研究结果显示,当HashMap数组长度为512的时候,也就是用掩码取低9位的时候,在没有使用hash()的情况下,发生了103次碰撞,接近30%。而在使用了hash()之后只有92次碰撞。碰撞减少了将近10%。看来扰hash()函数在将降低碰撞上还是有功效的。
hashMap中 MAXIMUM_CAPACITY = 1 << 30;最大为2的30次方(超过这个值就将threshold修改为Integer.MAX_VALUE(此时表的大小已经是2的31次方了),表明不进行扩容了)