HashMap里的一些知识点
1 Hash算法
HashMap里的k-v值如何计算得到索引:先看算法如下,以jdk1.8为例:
int index = hash & (arrays.length-1);
hash为key的HashCode计算得到的,为什么HashMap的数组长度是2的整数幂呢,因为,以初始长度为16为例,16-1 = 15,15的二进制数位00000000 00000000 00001111。可以看出一个基数二进制最后一位必然位1,当与一个hash值进行与运算时,最后一位可能是0也可能是1。但偶数与一个hash值进行与运算最后一位必然为0,造成有些位置永远映射不上值。
再来看看hash这个值如何得到:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
上述这个算法为了解决即使散列函数很松散,但只取最后几位碰撞也会很严重的问题,这时候hash算法的价值就体现出来了,hashCode右移16位,正好是32bit的一半。与自己本身做异或操作(相同为0,不同为1)。就是为了混合哈希值的高低位,增加低位的随机性。并且混合后的值也变相保持了高位的特征。图示如下:
2 扩容方式
源码解析如下:
//说明,hashMap本次扩容之前,table不为null if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { //当前node节点 Node<K,V> e; //说明当前桶位中有数据,但是数据具体是 单个数据,还是链表 还是 红黑树 并不知道 if ((e = oldTab[j]) != null) { //方便JVM GC时回收内存 oldTab[j] = null; //第一种情况:当前桶位只有一个元素,从未发生过碰撞,这情况 直接计算出当前元素应存放在 新数组中的位置,然后 //扔进去就可以了 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //第二种情况:当前节点已经树化,这个看底下,这里先主要看链表 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order //第三种情况:桶位已经形成链表 //低位链表:存放在扩容之后的数组的下标位置,与当前数组的下标位置一致。 Node<K,V> loHead = null, loTail = null; //高位链表:存放在扩容之后的数组的下表位置为 当前数组下标位置 + 扩容之前数组的长度 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //hash-> .... 1 1111 //hash-> .... 0 1111 // 0b 10000 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; } } } }
3 HashMap里的红黑树
HashMap在树化之后仍然保留了next节点,并且有pre节点指向链表时上一个元素,我们仍然可以按遍历链表的方式去遍历上面的红黑树。这样的结构为后面红黑树的切分以及红黑树转成链表做好了准备
红黑树在扩容时的split方法如下:
// 红黑树转链表阈值 static final int UNTREEIFY_THRESHOLD = 6; final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0; /* * 红黑树节点仍然保留了 next 引用,故仍可以按链表方式遍历红黑树。 * 下面的循环是对红黑树节点进行分组,与上面类似 */ for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } if (loHead != null) { // 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表 if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { tab[index] = loHead; /* * hiHead == null 时,表明扩容后, * 所有节点仍在原位置,树结构不变,无需重新树化 */ if (hiHead != null) loHead.treeify(tab); } } if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } }
扩容后,普通节点需要重新映射,红黑树节点也不例外。按照一般的思路,我们可以先把红黑树转成链表,之后再重新映射链表即可。这种处理方式是大家比较容易想到的,但这样做会损失一定的效率。不同于上面的处理方式,HashMap 实现的思路则很好。如上节所说,在将普通链表转成红黑树时,HashMap 通过两个额外的引用 next 和 prev 保留了原链表的节点顺序。这样再对红黑树进行重新映射时,完全可以按照映射链表的方式进行。这样就避免了将红黑树转成链表后再进行映射,无形中提高了效率。
重新映射后,会将红黑树拆分成两条由 TreeNode 组成的链表。如果链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。否则根据条件重新将 TreeNode 链表树化。