我们想象一下,假如用 key 原来的hashCode值,直接和 (n-1) 进行与运算来求数组下标,而不进行高低位混合运算,会产生什么样的结果。
//例如我有另外一个h2,和原来的 h相比较,高16位有很大的不同,但是低16位相似度很高,甚至相同的话。 //原h值 0110 1101 0110 1111 0110 1110 0010 1000 //另外一个h2值 0100 0101 1110 1011 0110 0110 0010 1000 // n -1 ,即 15 的二进制 0000 0000 0000 0000 0000 0000 0000 1111 //可以发现 h2 和 h 的高位不相同,但是低位相似度非常高。 //他们分别和 n -1 进行与运算时,得到的结果却是相同的。(此处n假设为16) //因为 n-1 的高16位都是0,不管 h 的高 16 位是什么,与运算之后,都不影响最终结果,高位一定全是 0 //因此,哈希碰撞的概率就大大增加了,并且 h 的高16 位特征全都丢失了。爱思考的同学可能就会有疑问了,我进行高低16位混合运算,是可以的,这样可以保证尽量减少高区位的特征。那么,为什么选择用异或运算呢,我用与、或、非运算不行吗?
这是有一定的道理的。我们看一个表格,就能明白了。
可以看到两个值进行与运算,结果会趋向于0;或运算,结果会趋向于1;而只有异或运算,0和1的比例可以达到1:1的平衡状态。(非呢?别扯犊子了,两个值怎么做非运算。。。)
所以,异或运算之后,可以让结果的随机性更大,而随机性大了之后,哈希碰撞的概率当然就更小了。
以上,就是为什么要对一个hash值进行高低位混合,并且选择异或运算来混合的原因。
resize() 扩容机制在上边 put 方法中,我们会发现,当数组为空的时候,会调用 resize 方法,当数组的 size 大于阈值的时候,也会调用 resize方法。 那么看下 resize 方法都做了哪些事情吧。
final Node<K,V>[] resize() { //旧数组 Node<K,V>[] oldTab = table; //旧数组的容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; //旧数组的扩容阈值,注意看,这里取的是当前对象的 threshold 值,下边的第2种情况会用到。 int oldThr = threshold; //初始化新数组的容量和阈值,分三种情况讨论。 int newCap, newThr = 0; //1.当旧数组的容量大于0时,说明在这之前肯定调用过 resize扩容过一次,才会导致旧容量不为0。 //为什么这样说呢,之前我在 tableSizeFor 卖了个关子,需要注意的是,它返回的值是赋给了 threshold 而不是 capacity。 //我们在这之前,压根就没有在任何地方看到过,它给 capacity 赋初始值。 if (oldCap > 0) { //容量达到了最大值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //新数组的容量和阈值都扩大原来的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //2.到这里,说明 oldCap <= 0,并且 oldThr(threshold) > 0,这就是 map 初始化的时候,第一次调用 resize的情况 //而 oldThr的值等于 threshold,此时的 threshold 是通过 tableSizeFor 方法得到的一个2的n次幂的值(我们以16为例)。 //因此,需要把 oldThr 的值,也就是 threshold ,赋值给新数组的容量 newCap,以保证数组的容量是2的n次幂。 //所以我们可以得出结论,当map第一次 put 元素的时候,就会走到这个分支,把数组的容量设置为正确的值(2的n次幂) //但是,此时 threshold 的值也是2的n次幂,这不对啊,它应该是数组的容量乘以加载因子才对。别着急,这个会在③处理。 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //3.到这里,说明 oldCap 和 oldThr 都是小于等于0的。也说明我们的map是通过默认无参构造来创建的, //于是,数组的容量和阈值都取默认值就可以了,即 16 和 12。 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //③ 这里就是处理第2种情况,因为只有这种情况 newThr 才为0, //因此计算 newThr(用 newCap即16 乘以加载因子 0.75,得到 12) ,并把它赋值给 threshold if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //赋予 threshold 正确的值,表示数组下次需要扩容的阈值(此时就把原来的 16 修正为了 12)。 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //如果原来的数组不为空,那么我们就需要把原来数组中的元素重新分配到新的数组中 //如果是第2种情况,由于是第一次调用resize,此时数组肯定是空的,因此也就不需要重新分配元素。 if (oldTab != null) { //遍历旧数组 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //取到当前下标的第一个元素,如果存在,则分三种情况重新分配位置 if ((e = oldTab[j]) != null) { oldTab[j] = null; //1.如果当前元素的下一个元素为空,则说明此处只有一个元素 //则直接用它的hash()值和新数组的容量取模就可以了,得到新的下标位置。 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //2.如果是红黑树结构,则拆分红黑树,必要时有可能退化为链表 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //3.到这里说明,这是一个长度大于 1 的普通链表,则需要计算并 //判断当前位置的链表是否需要移动到新的位置 else { // preserve order // loHead 和 loTail 分别代表链表旧位置的头尾节点 Node<K,V> loHead = null, loTail = null; // hiHead 和 hiTail 分别代表链表移动到新位置的头尾节点 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //如果当前元素的hash值和oldCap做与运算为0,则原位置不变 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } //否则,需要移动到新的位置 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); //原位置不变的一条链表,数组下标不变 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //移动到新位置的一条链表,数组下标为原下标加上旧数组的容量 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }