HashMap的底层数组长度总是2的n次方,在构造函数中存在:capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length - 1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。至于为什么是2的n次方下面解释。
我们回到indexFor方法,该方法仅有一条语句:h&(length - 1) 作用:均匀分布table数据和充分利用空间。
这里我们假设length为16(2^n)和15,h为5、6、7。
h length - 1 h & length -1
5 15 0101 & 1111 = 00101 5
6 15 0110 & 1111 = 00110 6
7 15 0111 & 1111 = 00111 7
length = 15
5 14 0101 & 1110 = 00101 5
6 14 0110 & 1110 = 00110 6
7 14 0111 & 1110 = 00110 6
当n=15时,6和7的结果一样,这样表示他们在table存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,这样就会导致查询速度降低。诚然这里只分析三个数字不是很多,那么我们就看0-15。
h length - 1 h & length - 10 14 0000 & 1110 = 0000 0
1 14 0001 & 1110 = 0000 0
2 14 0010 & 1110 = 0010 2
3 14 0011 & 1110 = 0010 2
4 14 0100 & 1110 = 0100 4
5 14 0101 & 1110 = 0100 4
6 14 0110 & 1110 = 0110 6
7 14 0111 & 1110 = 0110 6
8 14 1000 & 1110 = 1000 8
9 14 1001 & 1110 = 1000 8
10 14 1010 & 1110 = 1010 10
11 14 1011 & 1110 = 1010 10
12 14 1100 & 1110 = 1100 12
13 14 1101 & 1110 = 1100 12
14 14 1110 & 1110 = 1110 14
15 14 1111 & 1110 = 1110 14
从上面的图表中我们看到总共发生了8此碰撞,同时发现浪费的空间非常大,有1、3、5、7、9、11、13、15处没有记录,也就是没有存放数据。这是因为他们在与14进行&运算时,得到的结果最后一位永远都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的,空间减少,进一步增加碰撞几率,这样就会导致查询速度慢。而当length = 16时,length – 1 = 15 即1111,那么进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。
这里我们再来复习put的流程:当我们想一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其key的hash值。如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。具体的实现过程见addEntry方法,如下:
// 获取bucketIndex处的Entry
Entry<K,V> e = table[bucketIndex];
// 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 若HashMap中元素的个数超过极限了,则容量扩大两倍
if (size++ >= threshold)
resize(2 * table.length);
}