前言
日常开发中,经常会使用到JDK自带的集合类:List、Set、Map三者的实现,ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等。其中List的实现相对简单,ArrayList 底层基于数组,LinkedList 基于链表。HashSet 借助 HashMap 实现,TreeSet 借助 TreeMap 实现,仅利用到了Map中的key(例如add方法,调用Map.put(key,new Object())),凭借 Map 自带的key唯一特性,Set 很容易实现了去重特性,因此只需要分析 Map 的实现即可。
说到这里,目标已经很明确了,要分析 Map。那再来对比下 TreeMap 和 HashMap 的区别,前者借助“红黑树”数据结构实现元素有序,但阅读过 HashMap 源码的同学应该知道,HashMap 也实现了“红黑树”,所以本文的目标就是——HashMap 的源码分析。
文中涉及的图是从网上获取的。
目标
将对如下3个常用的方法进行源码分析:
- V put(K key, V value)
- V get(Object key)
- V remove(Object key)
源码分析
上帝视角
首先让我们站在上帝视角去观察 HashMap 的几个主要成员变量:
<1>table:table 就是要存储数据的结构,它是一个 Node 数组。因为 HashMap 利用 hash 散列的特性对数据进行存取,所以节点维护了自身的 hash 值;Node 自身通过维护后驱节点,又实现了链表的功能;Map 本身就是 key-value 的存储结构,所以还有 key和value两个成员变量。
<2>size:当前存储的 key-value 个数。
<3>threshold:英文解释门槛,也就是达到这个界限时,Map会进行扩容。
<4>loadFactor:负载因子,会在下面具体分析。
public class HashMapextends AbstractMap implements Map , Cloneable, Serializable { transient Node [] table; transient int size; int threshold; final float loadFactor; static class Node implements Map.Entry { final int hash; final K key; V value; Node next; } static final class TreeNode extends LinkedHashMap.Entry { TreeNode parent; TreeNode left; TreeNode right; TreeNode prev; boolean red; }}
这里还有一个静态内部类 TreeNode,继承自 LikedHashMap.Entry,LikedHashMap.Entry又继承自 HashMap.Node,所以它实际还是一个 Node,不过特殊点,它实现了红黑树的相关操作,这个在后面也会讲到。
新增—put
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // Node数组局部变量 Node[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) // 如果table还未创建,需要初始化 n = (tab = resize()).length; // 如果这时对应位置还没有节点,则新建 if ((p = tab[i = (n - 1) & hash]) == null) // new Node<>(hash, key, value, next) tab[i] = newNode(hash, key, value, null); else { // e:最终需要赋值的节点 Node e; K k; // 如果hash值相同,且key也相同,说明是替换 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // TreeNode是红黑树实现,用于解决链条长度过长,加速插入和查询 else if (p instanceof TreeNode) // 红黑树插入 e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); // 如果不在table上,说明在链条上 else { for (int binCount = 0; ; ++binCount) { // 顺着链表直到最后一个节点还没有找到,则新建 if ((e = p.next) == null) { // new Node<>(hash, key, value, next) p.next = newNode(hash, key, value, null); // 如果该链表长度大于指定值(默认是8) if (binCount >= TREEIFY_THRESHOLD - 1) // 红黑树初始化构建 treeifyBin(tab, hash); break; } // 过程中找到了,需要替换 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 如果e不为null,说明之前就存在该key了 if (e != null) { // 返回旧值,并放入新值 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 如果 size > capacoty * load factor,进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}
因为 HashMap 的结构是 Node[]数组,一个 key-value 的插入会涉及到索引计算,是通过 hash(key) & (数组长度-1)。之后再自成链条。如果是 HashMap 创建后的首次put,因为 table 变量还未赋初值,所以需要有初始化的操作—resize(),当然也是后期达到阈值(threshold)的扩容操作。
final Node[] resize() { Node [] oldTab = table; // 涉及到几个变量,oldCap-旧容量,oldThr-旧阈值,newCap-新容量,newThr-新阈值 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // oldCap>0对应扩容操作 if (oldCap > 0) { // 最大容量判断 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 将旧容量*2,作为新容量,且旧容量需要达到默认初始容量(16) else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 旧阈值*2,作为新阈值 newThr = oldThr << 1; } // --用户手动指定了初始容量处理,下面会分析-- else if (oldThr > 0) newCap = oldThr; // 未指定初始容量,则使用默认值 else { // 容量:16,阈值:16*0.75=12 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // --用户手动指定了初始容量处理,下面会分析-- if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 成员变量 threshold赋值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 使用得到的新容量,创建 Node数组 Node [] newTab = (Node [])new Node[newCap]; table = newTab; // 将原来的数据重新 hash放入新数组 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode )e).split(this, newTab, j, oldCap); else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node 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; } } } } } return newTab;}
这里有种情况,就是如果我们手动指定了初始值的特殊处理,比如 new HashMap<String,String>(12),那它的初始容量就是12么?既然这么问了,那必然不是的,HashMap会自动帮我们转为16(也就是和12最接近的2的整次方)。转换方法见方法 tableSizeFor,它会将计算结果暂存到 threshold 中,在 resize 中以初始容量赋值给 newCap。
为什么容量必须为2的正次方呢?让我们回想下之前的索引定位,hash(key) & (数组长度-1)。就拿16来举例,“16-1”对应的二进制应该是:01111,如果按位与的话,恰好就是 hash(key)%(length-1) 的取模值,计算机天生对位处理就是高效的!
到这里,put方法分析基本结束,让我们来看get方法。(这里涉及到的红黑树构建、插入会在后面讲解)
获取—get
public V get(Object key) { Nodee; return (e = getNode(hash(key), key)) == null ? null : e.value;}
final NodegetNode(int hash, Object key) { Node [] tab; Node first, e; int n; K k; // hash查找索引位置 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { // 如果是红黑节点 if (first instanceof TreeNode) // 涉及到红黑树查找 return ((TreeNode )first).getTreeNode(hash, key); // 链表遍历查找 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}
根据之前的 put操作,我们也能大致猜想到 get基本就是 put 的反操作,怎么放就怎么拿。(这里涉及到的红黑树查询会在后面讲解)
删除—remove
public V remove(Object key) { Nodee; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;}
final NoderemoveNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node [] tab; Node p; int n, index; // 根据hash值找索引 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node node = null, e; K k; V v; // 如果table中存放的就是要找的key if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 赋值给node用于返回 node = p; // 延链表向后遍历 else if ((e = p.next) != null) { // 如果是红黑节点,涉及到红黑树查找 if (p instanceof TreeNode) node = ((TreeNode )p).getTreeNode(hash, key); else { // 否则就逐一对比 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 如果找到的节点是红黑节点 if (node instanceof TreeNode) // 涉及到红黑树删除 ((TreeNode )node).removeTreeNode(this, tab, movable); // 如果移除的节点在table上,需要将next放到table上 else if (node == p) tab[index] = node.next; // 如果移除的节点在链表上,则需要断链重连 else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null;}
删除操作主要分为两步:
- 第一步查询,即找到要删除的节点;
- 第二步删除,红黑树除外,链表形式的无非就是节点指向重连。
到此为止,除了红黑树内容,基本已经将 HashMap的主要内容分析完毕。大致就是对 Node[]数据,以及 Node 链表的操作,没有很难理解的代码(这里涉及到红黑树删除操作)。接下来将主要分析“红黑树”的相关操作。
红黑树
红黑树特性
- 每个节点非红即黑;
- 根节点是黑色;
- 如果一个节点是红色的,则它的子节点必须是黑色的,也就是根到叶子节点的任何路径不能连续出现两个红节点,但黑节点没有限制;
- 从任一节点到该节点的叶子节点的所有路径上包含相同数目的黑节点;
- 其余满足二叉查找树特性(左节点<父节点<右节点)。
红黑树构建—treeifyBin
构建的第一步就是将原本的 Node 节点,替换为 TreeNode 节点。
final void treeifyBin(Node[] tab, int hash) { int n, index; Node e; // 检查是否需要扩容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { // hd用于存放根 TreeNode hd = null, tl = null; do { // 替换为红黑树节点:new TreeNode<>(p.hash, p.key, p.value, next) TreeNode p = replacementTreeNode(e, null); if (tl == null) hd = p; else { // 建立双向关系 p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); // 从根节点往下开始构建 if ((tab[index] = hd) != null) // 红黑树构建 hd.treeify(tab); }}
这一步仅涉及到节点的替换,还未涉及红黑树的节点之间的连接,具体见 TreeNode.treeify 方法。
final void treeify(Node[] tab) { TreeNode root = null; // 这里的this表示根 for (TreeNode x = this, next; x != null; x = next) { // 根据之前构造的双向链表遍历 next = (TreeNode )x.next; x.left = x.right = null; // 根节点指定为黑节点 if (root == null) { x.parent = null; x.red = false; root = x; } // 非根节点逻辑 else { K k = x.key; int h = x.hash; Class kc = null; for (TreeNode p = root;;) { int dir, ph; K pk = p.key; // 父hash大,dir=-1 if ((ph = p.hash) > h) dir = -1; // 子hash大,dir=-1 else if (ph < h) dir = 1; // 如果hash值相同,则需要比较class对象的hash值 else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) // 判断两者hash大小,前者>=后者,返回1,否则返回-1,不能比较返回0 dir = tieBreakOrder(k, pk); TreeNode xp = p; // 要插入的位置没有子节点,则插入 if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) // hash值小于父的hash,放到左子树 xp.left = x; else // hash值大于父的hash,放到右子树 xp.right = x; // 涉及到红黑树插入后平衡 root = balanceInsertion(root, x); break; } // 如果要插入的地方有节点,则继续向下遍历 } } } // 确保当前root是直接落到table数组上的 moveRootToFront(tab, root);}
整体来看,可以等同于二叉查找树的构建,即满足性质,左子树的所有节点<父节点<右子树所有节点。最后的 balanceInsertion 会对树进行调整,以满足红黑树性质。(默认插入节点为红,因为这样能减低调整的几率)
从插入的节点不满足红黑树性质的场景,我们分析并枚举下来如下,只罗列了父节点是祖父的左子节点情况,右子节点的情况对称操作即可:
- 1:插入节点的父节点和叔节点为红;
- 2:插入节点为父节点(红)的左子节点,叔节点为黑;
- 3:插入节点为父节点(红)是右子节点,叔节点为黑。
其中情况1处理如下:
只需要进行变色,就是把父和叔节点都变黑,祖父变红。之后再以祖父为基准,继续向上变换。
情况2处理如下:
即右旋操作,可以形象的理解为,拎着父节点提起来,之后变色(对应情况1)。
情况3处理如下:
这一步稍微麻烦一点,因为直接拎P可能有点问题,需要先把N拎到P的位置,之后再拎。也就是先左旋,再右旋的操作(对应情况2),再变色(对应情况1)。
以上3种情况即可覆盖所有的插入场景,保证一次插入在最多3次调整后,即可满足红黑树性质。了解了大致操作后,我们来看源码:
staticTreeNode balanceInsertion(TreeNode root, TreeNode x) { // 将插入的节点设为红色 x.red = true; for (TreeNode xp, xpp, xppl, xppr;;) { // 如果插入节点为root,置黑并返回 if ((xp = x.parent) == null) { x.red = false; return x; } // 如果父节点为黑 or 父节点是根节点,不需要做任何操作 else if (!xp.red || (xpp = xp.parent) == null) return root; // 如果父节点(红)是左分支 if (xp == (xppl = xpp.left)) { // 如果叔叔为红色 if ((xppr = xpp.right) != null && xppr.red) { // 叔叔变黑 xppr.red = false; // 父变黑 xp.red = false; // 祖父变红 xpp.red = true; // 将x赋值为祖父,继续向上调整 x = xpp; } // 叔叔不存在,或为黑色 else { // 插入节点在右分支 if (x == xp.right) { // 将父亲节点执行一次左旋 root = rotateLeft(root, x = xp); // 旋转后重新修正x、xp、xpp的指向 xpp = (xp = x.parent) == null ? null : xp.parent; } // 插入节点在左分支,无须调整 if (xp != null) { // 父变黑 xp.red = false; if (xpp != null) { // 祖父变红 xpp.red = true; // 对祖父进行右旋 root = rotateRight(root, xpp); } } } } // 如果父节点是右分支,对称操作 else { // 叔叔为红色 if (xppl != null && xppl.red) { // 叔叔变黑 xppl.red = false; // 父变黑 xp.red = false; // 祖父变红 xpp.red = true; // x置为祖父向上调整 x = xpp; } else { // 插入节点在左分支 if (x == xp.left) { // 将父亲节点执行一次右旋 root = rotateRight(root, x = xp); // 旋转后重新修正x、xp、xpp的指向 xpp = (xp = x.parent) == null ? null : xp.parent; } // 插入节点在右分支,无须调整 if (xp != null) { // 父变黑 xp.red = false; if (xpp != null) { // 祖父变红 xpp.red = true; // 对祖父进行左旋 root = rotateLeft(root, xpp); } } } } }}
可以看到除了插入root节点,以及插入节点的父节点为黑的简单处理外,其余处理一分为二,一部分是父节点在左分支的处理,另一部分是在右分支的处理,基本可看作对称的操作。里面具体的操作上面图示已经讲解过了,大家对应看看即可。
红黑树插入——putTreeVal
final TreeNodeputTreeVal(HashMap map, Node [] tab, int h, K k, V v) { Class kc = null; boolean searched = false; // 获取到根节点 TreeNode root = (parent != null) ? root() : this; for (TreeNode p = root;;) { int dir, ph; K pk; // 根据hash值大小选择左右分支 if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; // 如果找到hash值相同且key相同的,则返回 else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { if (!searched) { TreeNode q, ch; searched = true; // 递归查询 if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null)) return q; } // class对象的比较 dir = tieBreakOrder(k, pk); } TreeNode xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { // 创建节点并根据 dir值选择是左子节点还是右子节点 Node xpn = xp.next; TreeNode x = map.newTreeNode(h, k, v, xpn); if (dir <= 0) xp.left = x; else xp.right = x; // 链表连接 xp.next = x; x.parent = x.prev = xp; if (xpn != null) ((TreeNode )xpn).prev = x; // 插入后平衡操作,以及保证root节点在table上 moveRootToFront(tab, balanceInsertion(root, x)); return null; } }}
插入操作依照二叉查找树性质来判断插入节点的位置,插入后的平衡操作上面已经介绍过了。
红黑树查找——getTreeNode
final TreeNodegetTreeNode(int h, Object k) { return ((parent != null) ? root() : this).find(h, k, null);}
final TreeNodefind(int h, Object k, Class kc) { TreeNode p = this; do { int ph, dir; K pk; TreeNode pl = p.left, pr = p.right, q; // 根据hash值大小选择,小走左分支 if ((ph = p.hash) > h) p = pl; // 大走右分支 else if (ph < h) p = pr; // 如果hash相同,则需要比较key值,相同则返回 else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; // 左分支为空则向右找 else if (pl == null) p = pr; // 右分支为空则向左找 else if (pr == null) p = pl; // 到这里说明,红黑树节点有hash相同的,那么需要比较class,来确定左还是右 // 跟构建红黑树的逻辑相对应 else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0) p = (dir < 0) ? pl : pr; // 递归 else if ((q = pr.find(h, k, kc)) != null) return q; else p = pl; } while (p != null); return null;}
查找也没什么难度,依靠二叉查找树的相关性质,通过判断hash(key)的大小,来选择左右分支。因为红黑树的自身性质,限制了树不会出现严重失衡的情况,所以限制了查询的最差下限。
红黑树删除——removeTreeNode
在分析源码前,依然来枚举出所有可能出现的情况:
- 1:删除节点无子节点
- 2:删除节点有一个子节点
- 3:删除节点有两个子节点
这里的情况1,直接删除即可。但需要考虑删除节点的颜色,如果是红色无需重构;如果是黑色节点,则需要重构。
情况2,直接使用子节点替换删除节点即可。但需要考虑删除节点的颜色,如果是红色节点,则无需重构(因为子节点必然是黑色);如果为黑色,则需要重构。
情况3:我们需要找到一个后继节点去替换它。满足条件的就是删除节点的右子树中最小节点(即延右子树的左节点一直走下去的最后一个节点)。这样需要删除的节点情况就变成了情况1和2的其中之一。
所以最终我们需要分析的就是情况1和2,以及删除后红黑树性质的维持。
final void removeTreeNode(HashMapmap, Node [] tab, boolean movable) { int n; if (tab == null || (n = tab.length) == 0) return; int index = (n - 1) & hash; // 指向table上节点(root节点) TreeNode first = (TreeNode )tab[index], root = first, rl; // 指向待删除节点的后驱节点 TreeNode succ = (TreeNode )next, pred = prev; // 如果删除节点为根,则将后驱节点放大到table上即可 if (pred == null) tab[index] = first = succ; // 否则断链重连即可(正向) else pred.next = succ; // 如果后驱节点不为空,也要断链重连(逆向) if (succ != null) succ.prev = pred; // 如果table上已经为空,直接返回 if (first == null) return; if (root.parent != null) root = root.root(); // 满足如下条件,就需要将红黑树解体了 if (root == null || root.right == null || (rl = root.left) == null || rl.left == null) { tab[index] = first.untreeify(map); return; } // 以下为红黑树内部调整 // p指向待删除的节点,replacement存放等待替换的接待点 TreeNode p = this, pl = left, pr = right, replacement; // 左右儿子都不为空 if (pl != null && pr != null) { TreeNode s = pr, sl; // 寻找右子树最左叶子节点作为后继 while ((sl = s.left) != null) s = sl; // 交换后继节点和待删除节点的颜色 boolean c = s.red; s.red = p.red; p.red = c; TreeNode sr = s.right; TreeNode pp = p.parent; // 如果后继就是右儿子(说明右子树只有一个节点) if (s == pr) { // 直接交换位置 p.parent = s; s.right = p; } else { // 否则需要重新修正指向关系 TreeNode sp = s.parent; if ((p.parent = sp) != null) { if (s == sp.left) sp.left = p; //p放到s原本的位置 else sp.right = p; } if ((s.right = pr) != null) pr.parent = s; //s放到p原本的位置 } p.left = null; if ((p.right = sr) != null) sr.parent = p; //s原本的右子树成为p的右子树 if ((s.left = pl) != null) pl.parent = s; //s原本的左子树成为p的左子树 if ((s.parent = pp) == null) root = s; //若p原本是根则新的根是s else if (p == pp.left) pp.left = s; //若p是某个结点的左儿子,则s成为该结点的左儿子 else pp.right = s; //若p是某个结点的右儿子,则s成为该结点的右儿子 //若s结点有右儿子(s一定没有左儿子),则replacement为这个右儿子否则为p if (sr != null) replacement = sr; else replacement = p; } // 左、右只有一个存在,直接替换父即可 else if (pl != null) replacement = pl; else if (pr != null) replacement = pr; // 没有儿子 else replacement = p; // 如果替换的节点不是自己 if (replacement != p) { TreeNode pp = replacement.parent = p.parent; // 如果删除的是根节点,则自己作为根 if (pp == null) root = replacement; // 将父节点对应的左右节点指向新节点 else if (p == pp.left) pp.left = replacement; else pp.right = replacement; p.left = p.right = p.parent = null; } // 之后会修复红黑树 TreeNode r = p.red ? root : balanceDeletion(root, replacement); // 如果替换的节点是自己 if (replacement == p) { TreeNode pp = p.parent; p.parent = null; if (pp != null) { // 将父节点对应的左右节点置空 if (p == pp.left) pp.left = null; else if (p == pp.right) pp.right = null; } } if (movable) moveRootToFront(tab, r);}
修复红黑树的方法为 balanceDeletion,除去一些不存在的情况(即删除前就违背红黑树性质的情况),仅分析一侧,将会有4种情况,在代码里已经涉及。
staticTreeNode balanceDeletion(TreeNode root, TreeNode x) { for (TreeNode xp, xpl, xpr;;) { if (x == null || x == root) return root;//删除结点为空或者删除的是根结点,直接返回 else if ((xp = x.parent) == null) { x.red = false;//删除后x成为根结点,x的颜色改为黑色 return x; } else if (x.red) { x.red = false;//将一个红色的结点提升到删除结点的位置不会改变黑高 return root; } else if ((xpl = xp.left) == x) {//x的父亲是左儿子 if ((xpr = xp.right) != null && xpr.red) { //情况1:x的兄弟是红色的 xpr.red = false; xp.red = true; root = rotateLeft(root, xp); xpr = (xp = x.parent) == null ? null : xp.right; } if (xpr == null) x = xp;//若x没有兄弟,x上升到父亲的位置 else { TreeNode sl = xpr.left, sr = xpr.right; if ((sr == null || !sr.red) && (sl == null || !sl.red)) { //情况2:x兄弟是黑色,他的两个儿子是黑色的 xpr.red = true; x = xp; } else { if (sr == null || !sr.red) { //情况3:x兄弟是黑色,它的右儿子是黑色,左儿子红色 if (sl != null) sl.red = false; xpr.red = true; root = rotateRight(root, xpr); xpr = (xp = x.parent) == null ? null : xp.right; } //情况4:x兄弟是黑色,它的右儿子是红色的 if (xpr != null) { xpr.red = (xp == null) ? false : xp.red; if ((sr = xpr.right) != null) sr.red = false; } if (xp != null) { xp.red = false; root = rotateLeft(root, xp); } x = root; } } } else { //以下为对称操作 if (xpl != null && xpl.red) { xpl.red = false; xp.red = true; root = rotateRight(root, xp); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl == null) x = xp; else { TreeNode sl = xpl.left, sr = xpl.right; if ((sl == null || !sl.red) && (sr == null || !sr.red)) { xpl.red = true; x = xp; } else { if (sl == null || !sl.red) { if (sr != null) sr.red = false; xpl.red = true; root = rotateLeft(root, xpl); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl != null) { xpl.red = (xp == null) ? false : xp.red; if ((sl = xpl.left) != null) sl.red = false; } if (xp != null) { xp.red = false; root = rotateRight(root, xp); } x = root; } } } }}
到此为止,HashMap 的分析就结束了。
注意事项
-
死循环问题
HashMap 是非线程安全的,在高并发场景下,可能会出现死循环的情况。比如,两个线程同时去扩容(意味着链条重建),一个线程中途挂起后,另一个线程将原本的A->B重构为B->A,紧接着之前的线程再次获取到CPU时间分片执行,发现进入死循环A<->B。
-
散列问题
比如将一个类的hashCode方法重写为返回固定值,再或者是受到Hash Dos攻击,涌入大量key不同,但hash相同的键值,会导致HashMap急速扩张,并且数据集中在一个链条上,因为链条长度>8之后就会升级为红黑树结构,插入又涉及到红黑树的平衡,这样下去无疑会减慢插入速度,并消耗大量CPU资源。
总结
HashMap 作为日常开发常用的集合类,自身实现利用了数组、链表、红黑树等,理解后,基本看其它集合类库就会很轻松了。