HashMap之hash函数

487 阅读3分钟

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次方了),表明不进行扩容了)