背景
无论在HashMap中进行查找、插入还是删除操作,都需要计算key的hashCode(),然后根据hashcode值求得所在的数组索引。那么具体是如何求得的呢?
JDK1.8版本中的HashMap中,有这样几个字段:
transient Node<K,V>[] table;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
table
是hash桶数组,DEFAULT_INITIAL_CAPACITY
则是数组的默认大小,而且HashMap在扩容的时候都是扩大到两倍,所以其table.length
一直都是2的幂次方。而HashMap中的求key的桶索引方法则包括以下几步:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//取模
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length-1);
}
1、首先求得key的hash值(通过key.hashCode()
),
2、进行低位异或运算,
3、取模。
这里就会有几点疑问了,首先,table
的length
为什么必须是2的幂次方?其次,为什么要进行低位异或操作?以及通过位与的方式取模有什么用意呢?请看下面。
思考
首先,table的length为什么必须是2的幂次方?
我们都知道,取得hash数组索引的方法就是对hash值关于hash桶长度取模,这样,每个key可以相对分散地分布在hash桶当中。但是一般来说,key的hash值都非常大,在对length
进行取模的时候,效率很低。所以也就诞生了上面的位与方式的取模方法,数组长度减一相当于一个低位掩码。以数组初始长度16为例,16-1=15,二进制表示就是00000000 00000000 00000000 00001111,和一个hash值进行与的时候,只截取了最低的四位值:
00000000 00000000 00000000 00001111
& 11101010 00101000 10010101 10101110
-------------------------------------------------------
00000000 00000000 00000000 00001110
所以说,为了高效率地计算取模,使得table的长度为16,这样就可以利用低位掩码进行与运算。
这时候就有其他问题了,就算hash值分布得再松散,当只取最后几位的时候,碰撞也依然会很严重;而且如果hashCode( )函数如果设计得不好,分布上成等差数列式的分布,恰好最后几位呈现规律性重复,就完了。
这也就引入了下面一个问题:
在计算hash值的时候为什么要进行低位的异或操作呢?
第一、HashMap的实现方式依赖于hashCode函数的实现。特别是,hash值的低位应该均匀分布。如果在较低位上有许多冲突,则HashMap将会出现较大的桶碰撞几率。
第二、因为hashCode方法的实现超出了HashMap的控制范围(每个对象都可以有它自己的实现方式),各种不同的hashCode方法实现的质量参差不齐。
所以HashMap提供了一个额外的哈希函数,,可以称之为“扰动函数”,就是上面的hash函数。该函数的做法就是将hash值的高16位与低16位进行异或操作,混合了高位信息和低位信息,使得得到的新值更具有随机性。
参考
https://stackoverflow.com/questions/2414117/explanation-of-hashmaphashint-method
https://www.zhihu.com/question/20733617