![]()
我们知道 HashMap 的底层是由数组,链表,红黑树组成的,在 HashMap 做扩容操作时,除了把数组容量扩大为原来的两倍外,还会对所有元素重新计算 hash 值,因为长度扩大以后,hash值也随之改变。
如果是简单的 Node 对象,只需要重新计算下标放进去就可以了,如果是链表和红黑树,那么操作就会比较复杂,下面我们就来看下,JDK1.8 下的 HashMap 在扩容时对链表和红黑树做了哪些优化?
rehash 时,链表怎么处理?
假设一个 HashMap 原本 bucket 大小为 16。下标 3 这个位置上的 19, 3, 35 由于索引冲突组成链表。
![http://cdn.chaohang.top/20200602180032.png]()
当 HashMap 由 16 扩容到 32 时,19, 3, 35 重新 hash 之后拆成两条链表。
![http://cdn.chaohang.top/20200603091606.png]()
查看 JDK1.8 HashMap 的源码,我们可以看到关于链表的优化操作如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; 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; }
|
正常我们是把所有元素都重新计算一下下标值,再决定放入哪个桶,JDK1.8 优化成直接把链表拆成高位和低位两条,通过位运算来决定放在原索引处或者原索引加原数组长度的偏移量处。我们通过位运算来分析下。
先回顾一下原 hash 的求余过程:
![]()
再看一下 rehash 时,判断时做的位操作,也就是这句 e.hash & oldCap
:
![]()
再看下扩容后的实际求余过程:
![]()
这波操作是不是很666,为什么 2 的整数幂 - 1可以作 & 操作可以代替求余计算,因为 2 的整数幂 - 1 的二进制比较特殊,就是一串 11111,与这串数字 1 作 & 操作,结果就是保留下原数字的低位,去掉原数字的高位,达到求余的效果。2 的整数幂的二进制也比较特殊,就是一个 1 后面跟上一串 0。
HashMap 的扩容都是扩大为原来大小的两倍,从二进制上看就是给这串数字加个 0,比如 16 -> 32 = 10000 -> 100000,那么他的 n - 1 就是 15 -> 32 = 1111 -> 11111。也就是多了一位,所以扩容后的下标可以从原有的下标推算出来。差异就在于上图我标红的地方,如果标红处是 0,那么扩容后再求余结果不变,如果标红处是 1,那么扩容后再求余就为原索引 + 原偏移量。如何判断标红处是 0 还是 1,就是把 e.hash & oldCap
。
rehash 时,红黑树怎么处理?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| static final int UNTREEIFY_THRESHOLD = 6;
final Node<K,V>[] resize() { else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); }
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0;
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) { if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else {
tab[index] = loHead; 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); } } }
|
从源码可以看出,红黑树的拆分和链表的逻辑基本一致,不同的地方在于,重新映射后,会将红黑树拆分成两条链表,根据链表的长度,判断需不需要把链表重新进行树化。
源码系列文章
Java源码系列1——ArrayList
Java源码系列2——HashMap
Java源码系列3——LinkedHashMap
参考
HashMap 源码详细分析(JDK1.8)
本文首发于我的个人博客 https://chaohang.top
作者张小超
转载请注明出处
欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。
![]()