# 要思考的问题
HashMap 的底层数据结构 (节点结构,这种结构有什么优点)
如何处理 hash 冲突
怎么扩容?扩展机制是什么?
增删改查过程
链表到红黑树的转换过程,反之?
红黑树相关 (见另一篇数据结构之红黑树)
hash 计算
# 达到的目标
# 看之前要掌握的知识点
# 红黑树
# 看之前大体了解的知识点
# hash 算法
# Poisson 分布
poisson 分布
# 开始
# HashMap 的继承体系
AbstractMap: map 的抽象类,以最大限度的减少实现 Map 接口的类的工作量。
# hashMap 结构
# 字段解释
# 常量字段 (默认值字段)
DEFAULT_INITIAL_CAPACITY=1<<4: 默认的初始容量,默认是为 16, 必须是 2 的 n 次方。为什么呢?见扩容的方法。
DEFAULT_LOAD_FACTOR=0.75f: 默认的负载因子。它和哈希表的容量的乘积是决定是否重新 hash 的阈值。
TREEIFY_THRESHOLD=8: 使用树而不是链表的计数阈值。当桶的元素添加到具有至少这么多节点时,桶被转换为树。
UNTREEIFY_THRESHOLD=6: 用于在调整大小操作期间解除(拆分)桶的桶计数阈值。(untreeifying 不是一个英语单词,这里的以是非树化,即转换成普通列表的过程). 也就是说从树转换成普通的桶 (链表) 的阈值。
MAXIMUM_CAPACITY=1<<30: 最大的容量: 1<<30
,如果具有参数的任一构造函数隐式指定更高的值,则使用此参数。必须是 2 的 n 次方,小于等于 1<<30
MIN_TREEIFY_CAPACITY=64: 容器可以树化的最小容量 (否则,如果 bin 中的节点太多,则会调整表的大小.) 应该至少为 4 * TREEIFY_THRESHOLD,以避免调整大小和树化阈值之间的冲突.
# 类属性
table: transient HashMap.Node<K,V>[] table
; table 在首次使用时初始化,并根据需要调整大小。分配时,长度始终是 2 的幂。(我们还在一些操作中容忍长度为零,以允许当前不需要的自举机制)
entrySet: transient Set<Map.Entry<K,V>> entrySet
; 保存缓存的 entrySet.
size: transient int size
; map 中元素的数量。结构修改是那些改变 HashMap 中映射数量或以其他方式修改其内部结构(例如,rehash)的修改。此字段用于在 HashMap 的 Collection-views 上快速生成迭代器 (见 ConcurrentModificationException)
注意:这些字段都是 transient
的?为什么呢?
loadFactor: final float loadFactor;
hash 表的负载因子,在实例化 hashTable 的时候指定,该对象内不能变更 (final);
threshold: int threshold;
, 下一次调整容器大小的阈值. threshold=capacity * load factor
# HashMap 的两种节点
static class Node<K,V> implements Map.Entry<K,V>
它继承了 Map 的 Entry, 是对子类的行为规范。要求提供了 getKey (),getValue () 等常用方法。
链表节点的结构如下:
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 static class Node <K ,V > implements Map .Entry <K ,V > { final int hash; final K key; V value; HashMap.Node<K,V> next; Node(int hash, K key, V value, HashMap.Node<K,V> next) { this .hash = hash; this .key = key; this .value = value; this .next = next; } @Override public final K getKey () { return key; } @Override public final V getValue () { return value; } @Override public final String toString () { return key + "=" + value; } @Override public final int hashCode () { return Objects.hashCode(key) ^ Objects.hashCode(value); } @Override public final V setValue (V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals (Object o) { if (o == this ) return true ; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true ; } return false ; } }
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>
继承了其子类的 Entry, 子类的 Entry 继承了父类的 Node. 注意了,这里乍一看还挺乱。来张图吧。
这里呢,TreeNode 其实是 Node 的孙子,也就是说 HashMap 的树节点是链表节点的孙子辈儿的。
为什么要使两种节点有继承关系呢?为什么 TreeNode 不直接继承 Node 节点呢?
1 2 3 4 5 6 7 8 9 10 11 static final class TreeNode <K ,V > extends LinkedHashMap .Entry <K ,V > { HashMap.TreeNode<K,V> parent; HashMap.TreeNode<K,V> left; HashMap.TreeNode<K,V> right; HashMap.TreeNode<K,V> prev; boolean red; TreeNode(int hash, K key, V val, HashMap.Node<K,V> next) { super (hash, key, val, next); } }
# HashMap 增加方法 HashMap#put ()
1 2 3 4 5 6 7 8 9 10 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
这里就比较有看点了,1. 这里是 hashMap 的增加方法,增加方法里必然会遇到 hash 冲突的问题,我们等会看下 hash 冲突是如何处理的,还会涉及到扩容的问题,我们也要来看看他是怎么扩容的,扩容的过程中还会遇到普通的桶转换成树的过程。我们先来看下 hash 值是怎么计算出来的。
1 2 3 4 5 6 7 8 9 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
为什么这么做呢?见 HashMap 的 Hash 函数到底有什么意义
{.line-numbers} 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 72 final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); else { HashMap.Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof HashMap.TreeNode) e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value); else { for (int binCount = 0 ; ; ++binCount) { if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); 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; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
这段代码里中有三处重要的地方, resize()
, treeifyBin()
, putTreeNode()
, 接下来我们依次看下这三个方法。
# resize
{.line-numbers} 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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 final HashMap.Node<K, V>[] resize() { HashMap.Node<K, V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; if (oldCap > 0 ) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) { newThr = oldThr << 1 ; } } else if (oldThr > 0 ) { newCap = oldThr; } else { 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 = newThr; @SuppressWarnings({"rawtypes", "unchecked"}) HashMap.Node<K, V>[] newTab = (HashMap.Node<K, V>[]) new HashMap.Node[newCap]; table = newTab; if (oldTab != null ) { for (int j = 0 ; j < oldCap; ++j) { HashMap.Node<K, V> e; if ((e = oldTab[j]) != null ) { oldTab[j] = null ; if (e.next == null ) { newTab[e.hash & (newCap - 1 )] = e; } else if (e instanceof HashMap.TreeNode) { ((HashMap.TreeNode<K, V>) e).split(this , newTab, j, oldCap); } else { HashMap.Node<K, V> loHead = null , loTail = null ; HashMap.Node<K, V> hiHead = null , hiTail = null ; HashMap.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; } } } } } return newTab; }
看一个散列还算非常均匀的例子来看扩容过程。
那么进行扩容的过程是怎么样的呢?
以元素 1 和 12 为例,看扩容过程:
元素 1 的 hash 值为 49.(以 hashMap 计算 hash 值的方式得出。), 与 15 取 & 计算桶的下标为 1, 扩容后,与 31 取 &, 计算桶的下标为 17. 所以扩容前位置是 0,扩容后元素 1 的存放位置是 17。
代码中是怎么完成这个过程的呢?
和扩容前 hash 表的容量取 &, 得 49 & 16 = 16 > 0
(代码第 86-96 行), 新的桶的头节点 (对应代码里的 hiHead) 就是当前节点 1,尾节点 (hiTail) 赋为当前节点。然后进行下一次 do...while
循环,处理节点 12, 计算出节点 12 的 hash 值为 1569
, 进行计算 1569 & 16 = 0 == 0
原来桶的头结点是节点 12, 尾节点也是节点 12 (对应着代码第 76-86 行), 这样 hitail 和 loTail 均不为 null, 所以然后直接使用 newTab[j] = loHead;
和 newTab[j + oldCap] = hiHead;
的方式确定桶的位置。这个案例里,处理完节点 12 才会确定桶的位置。因为原来的表中下标为 1 的桶中有两个元素 1 和 12. 那桶里只有一个元素的怎么处理的呢? newTab[e.hash & (newCap - 1)] = e;
e 是当前节点,newCap 是新表的容量。
如果你想问为什么能使用 hash & olcCap==0?
来决定是 newTab[j]
还是 newTab[j+oldCap]
这种方式来确定新的桶的下标的话。 那么原因就是扩容使用的是 2 次幂的方式,容量是原来容量的 2 倍。所以就可以使用 hash & olcCap==0?
来判断了。
这个例子呢,演示了扩容过程中的链表的新增和扩容过程。再回头看 resize 方法,还有一种情况我们没有分析过。那就是
1 2 3 4 5 6 ... else if (e instanceof HashMap.TreeNode) { ((HashMap.TreeNode<K, V>) e).split(this , newTab, j, oldCap); } ...
{.line-numbers} 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 final void split (HashMap<K, V> map, HashMap.Node<K, V>[] tab, int index, int bit) { HashMap.TreeNode<K, V> b = this ; HashMap.TreeNode<K, V> loHead = null , loTail = null ; HashMap.TreeNode<K, V> hiHead = null , hiTail = null ; int lc = 0 , hc = 0 ; for (HashMap.TreeNode<K, V> e = b, next; e != null ; e = next) { next = (HashMap.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); } } }
将树重新穿换成链表的过程就比较简单了:
{.line-numbers} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 final HashMap.Node<K, V> untreeify (HashMap<K, V> map) { HashMap.Node<K, V> hd = null , tl = null ; for (HashMap.Node<K, V> q = this ; q != null ; q = q.next) { HashMap.Node<K, V> p = map.replacementNode(q, null ); if (tl == null ) hd = p; else tl.next = p; tl = p; } return hd; }
这里就是和红黑树相关的内容了,这里关键的是 split 调用了一个 treeify 的方法。这个方法同时也被 treeifyBin 调用了。所以 treeify 方法就和 treeifyBin 方法一块分享。
顺便提一嘴,他们有如下的关系:
其中蓝色的是红黑树的方法,黄色的是 HashMap 调用的方法。
# treeifyBin
{.line-numbers} 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 final void treeifyBin (HashMap.Node<K, V>[] tab, int hash) { int n, index; HashMap.Node<K, V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) { resize(); } else if ((e = tab[index = (n - 1 ) & hash]) != null ) { `HashMap.TreeNode<K, V> hd = null , tl = null ; do { HashMap.TreeNode<K, V> 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); }` } }
其实呢,这个 treeifyBin
方法还是做了一些将桶树化的前置操作,然后将装有 TreeNode
节点的桶交给了 treeify
方法去真正的转换为一棵红黑树。那我们接下来看下 treeify
方法。注意这个方法定义在 HashMap.TreeNode#treeify()
# treeify () 方法
{.line-numbers} 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 final void treeify (HashMap.Node<K, V>[] tab) { HashMap.TreeNode<K, V> root = null ; for (HashMap.TreeNode<K, V> x = this , next; x != null ; x = next) { next = (HashMap.TreeNode<K, V>) 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 (HashMap.TreeNode<K, V> p = root; ; ) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) { dir = -1 ; } else if (ph < h) { dir = 1 ; } else if ((kc == null && (kc = comparableClassFor(k)) == null ) || (dir = compareComparables(kc, k, pk)) == 0 ) { dir = tieBreakOrder(k, pk); } HashMap.TreeNode<K, V> xp = p; if ((p = (dir <= 0 ) ? p.left : p.right) == null ) { x.parent = xp; if (dir <= 0 ){ xp.left = x; } else { xp.right = x; } root = balanceInsertion(root, x); break ; } } } } moveRootToFront(tab, root); }
在这里呢,有 3 个方法没有仔细去说明,分别是 tieBreakOrder (),balanceInsertion () 和 moveRootToFront (tab, root), 注意,这三个方法在下面的 PutTreeVal 中也有调用。当然包括调整平衡的左旋 (rotateLeft), 右旋 (rotateRight) 方法。我们接着往下看吧。
# balanceInsertion 方法
在说这个方法之前,先总结下红黑树变换的 5 条规则。
规则 1: 红黑树为空树 ==> {直接插入当前节点,节点涂为黑色。 }
规则 2: 插入节点的父节点是黑色 ==> {直接插入当前节点. }
规则 3: 当前节点的父节点是红色,并且叔叔节点是红色。==> {父节点涂黑,叔叔节点涂黑,祖父节点涂红. }
规则 4: 当前节点的父节点是红色,叔叔是黑色,当前节点是父节点的右子树. ==> {当前节点的父节点作为新的当前节点,以新的当前节点左旋。 }
规则 5: 当前节点的父节点是红色,叔叔节点是黑色,当前节点是父节点的左子树. ==> {父节点变为黑色,祖父节点变为红色,以祖父节点为支点右旋. }
下面结合代码看 HashMap 是怎么实现上面这个 5 个规则的:
{.line-numbers} 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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 static <K, V> HashMap.TreeNode<K, V> balanceInsertion (HashMap.TreeNode<K, V> root, HashMap.TreeNode<K, V> x) { x.red = true ; for (HashMap.TreeNode<K, V> xp, xpp, xppl, xppr; ; ) { if ((xp = x.parent) == null ) { x.red = false ; return x; } 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 = xpp; } else { if (x == xp.right) { root = rotateLeft(root, x = xp); 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 = xpp; } else { if (x == xp.left) { root = rotateRight(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null ) { xp.red = false ; if (xpp != null ) { xpp.red = true ; root = rotateLeft(root, xpp); } } } } } }
# rotateLeft 左旋
这里的代码不能用语言描述,真的是只能意会不能言传啊。
{.line-numbers} 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 static <K, V> HashMap.TreeNode<K, V> rotateLeft2 (HashMap.TreeNode<K, V> root, HashMap.TreeNode<K, V> p) { HashMap.TreeNode<K, V> r, pp, rl; if (p != null && p.right != null ) { r = p.right; if (r.left != null ) { p.right = r.left; rl = r.left; rl.parent = p; } pp = p.parent; if (p.parent == null ) { r.parent = p.parent; (root = r).red = false ; } else if (pp.left == p) { pp.left = r; } else { pp.right = r; } r.left = p; p.parent = r; } return root; }
注意下,这里的代码是我修改之后,JDK 的源码看起来很精简,理解起来,啧啧啧。
MD, 来张图:
这里假设右孩子是有左孩子的。如果没有的话,那就直接去掉绿色的 rl 就好了。
# rotateRight
右旋的过程同理:
{,.line-numbers} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static <K, V> HashMap.TreeNode<K, V> rotateRight (HashMap.TreeNode<K, V> root, HashMap.TreeNode<K, V> p) { HashMap.TreeNode<K, V> l, pp, lr; if (p != null && (l = p.left) != null ) { if ((lr = p.left = l.right) != null ) lr.parent = p; if ((pp = l.parent = p.parent) == null ) (root = l).red = false ; else if (pp.right == p) pp.right = l; else pp.left = l; l.right = p; p.parent = l; } return root; }
这图啊,有空再做吧。今天太累了。
还有一个方法:
# moveRootToFront
{.line-numbers} 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 static <K, V> void moveRootToFront (HashMap.Node<K, V>[] tab, HashMap.TreeNode<K, V> root) { int n; if (root != null && tab != null && (n = tab.length) > 0 ) { int index = (n - 1 ) & root.hash; HashMap.TreeNode<K, V> first = (HashMap.TreeNode<K, V>) tab[index]; if (root != first) { HashMap.Node<K, V> rn; tab[index] = root; HashMap.TreeNode<K, V> rp = root.prev; if ((rn = root.next) != null ) { ((HashMap.TreeNode<K, V>) rn).prev = rp; } if (rp != null ) { rp.next = rn; } if (first != null ) { first.prev = root; } root.next = first; root.prev = null ; } assert checkInvariants (root) ; } }
# putTreeNode
{./line-bumbers} 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 final HashMap.TreeNode<K, V> putTreeVal (HashMap<K, V> map, HashMap.Node<K, V>[] tab, int h, K k, V v) { Class<?> kc = null ; boolean searched = false ; HashMap.TreeNode<K, V> root = (parent != null ) ? root() : this ; for (HashMap.TreeNode<K, V> p = root; ; ) { int dir, ph; K pk; if ((ph = p.hash) > h) { dir = -1 ; } else if (ph < h) { dir = 1 ; } 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) { HashMap.TreeNode<K, V> 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; } dir = tieBreakOrder(k, pk); } HashMap.TreeNode<K, V> xp = p; if ((p = (dir <= 0 ) ? p.left : p.right) == null ) { HashMap.Node<K, V> xpn = xp.next; HashMap.TreeNode<K, V> 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 ) ((HashMap.TreeNode<K, V>) xpn).prev = x; moveRootToFront(tab, balanceInsertion(root, x)); return null ; } } }
这样,HashMap 的新增过程我们就处理完了。
# HashMap 删除方法 HashMap#remove ()
{.line-numbers} 1 2 3 4 5 6 7 8 9 10 public V remove (Object key) { HashMap.Node<K, V> e; return (e = removeNode(hash(key), key, null , false , true )) == null ? null : e.value; }
其中计算 hash 值的方法还是和之前的一样。
# removeNode
{.line-numbers} 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 final HashMap.Node<K, V> removeNode (int hash, Object key, Object value, boolean matchValue, boolean movable) { HashMap.Node<K, V>[] tab; HashMap.Node<K, V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1 ) & hash]) != null ) { HashMap.Node<K, V> node = null , e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null ) { if (p instanceof HashMap.TreeNode) { node = ((HashMap.TreeNode<K, V>) 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 HashMap.TreeNode) { ((HashMap.TreeNode<K, V>) node).removeTreeNode(this , tab, movable); }else if (node == p) { tab[index] = node.next; }else { p.next = node.next; } ++modCount; --size; afterNodeRemoval(node); return node; } } return null ; }
还有一个最难理解的方法落在了红黑树的移除上了。
# HashMap#TreeNode#removeTreeNode
还是先看下红黑树的删除是怎么回事。
在删除方法调用之前必须要有存在的给定节点。
这比典型的红黑删除代码更混乱,因为我们不能将内部节点的内容与叶子后继交换,后者由遍历期间可独立访问的 “下一个” 指针固定。 所以我们交换树链接。 如果当前树似乎有太少的节点,则红黑树 (bin) 将转换回普通的链表 (普通 bin). (测试会在 2 到 6 个节点之间触发,具体取决于树结构)。
上面是 removeTreeNode 方法的解释。说实话,没理解…
HashMap 的删除不同于普通的红黑树的删除,因为它其中还维护了,一个链表的指向. HashMap 采用的是将树中的两个节点进行换位,颜色也要进行互换,来保证红黑树的平衡,并不改变二者在链表中的位置,互换后,删除节点此时的左子树是空的,将问题转换成了对左子树为空的节点的删除。
有一个简单的问题,千万不要弄混了,就是 TreeNode 中要删除的节点是谁??
删除的签名是这样的: final void removeTreeNode(HashMap<K, V> map, HashMap.Node<K, V>[] tab,boolean movable)
, 并没有传 TreeNode 啊?是不是??
干吗呢!大兄嘚。要删除的节点是:this 啊。我们现在走到了 TreeNode 内部了!!它本身就是要被删除的节点啊。
好了,那我现在要告诉你:删除自己!
HashMap 删除红黑树的节点,实际上就是 TreeNode 自己删除自己。那么它是怎么删的呢?
它分成了三步:
1. 将删除节点从双链向链表中删除.
2. 将删除节点与其右子树最小节点互换,之后平衡树
3. 将树根节点,移动到 tab[index]
指针处
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 final void removeTreeNode (HashMap<K, V> map, HashMap.Node<K, V>[] tab, boolean movable) { int n; if (tab == null || (n = tab.length) == 0 ) return ; int index = (n - 1 ) & hash; HashMap.TreeNode<K, V> first = (HashMap.TreeNode<K, V>) tab[index], root = first, rl; HashMap.TreeNode<K, V> succ = (HashMap.TreeNode<K, V>) next, pred = prev; if (pred == null ) { tab[index] = first = succ; } else { pred.next = succ; } if (succ != null ) { succ.prev = pred; } if (first == null ) { return ; } if (root.parent != null ) { root = root.root(); } if (root == null || (movable && (root.right == null || (rl = root.left) == null || rl.left == null ))) { tab[index] = first.untreeify(map); return ; } HashMap.TreeNode<K, V> p = this , pl = left, pr = right, replacement; if (pl != null && pr != null ) { HashMap.TreeNode<K, V> s = pr, sl = s.left; while (sl != null ) { s = sl; sl = s.left; } boolean c = s.red; s.red = p.red; p.red = c; HashMap.TreeNode<K, V> sr = s.right; HashMap.TreeNode<K, V> pp = p.parent; if (s == pr) { p.parent = s; s.right = p; } else { HashMap.TreeNode<K, V> sp = s.parent; if ((p.parent = sp) != null ) { if (s == sp.left) sp.left = p; else sp.right = p; } if ((s.right = pr) != null ) pr.parent = s; } p.left = null ; if ((p.right = sr) != null ) { sr.parent = p; } if ((s.left = pl) != null ) { pl.parent = s; } if ((s.parent = pp) == null ) { root = s; } else if (p == pp.left) { pp.left = s; } else { pp.right = s; } 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) { HashMap.TreeNode<K, V> 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 ; } HashMap.TreeNode<K, V> r = p.red ? root : balanceDeletion(root, replacement); if (replacement == p) { HashMap.TreeNode<K, V> 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); } }
这样呢,整个删除过程就完成了。
用官方中的话,比较混乱。尤其是涉及到红黑树的删除,这部分内容。还是需要好好消化,消化的。
下面我们还剩下两个内容:修改和查找
# HashMap 的查找方法
HashMap
的查找的方法,有 get
, getOrDefault
. 很明显,这两个方法前者不存在的时候返回的是 null
,后者返回的就是 defaultValue
.
先来看下这两个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public V get (Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } public V getOrDefault (Object key, V defaultValue) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? defaultValue : e.value; }
我们可以看到,它调用的都是同一个方法,顺藤摸瓜,我们看这个 getNode
方法.
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 final HashMap.Node<K, V> getNode (int hash, Object key) { HashMap.Node<K, V>[] tab; HashMap.Node<K, V> first, e; int n; K k; 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 HashMap.TreeNode) { return ((HashMap.TreeNode<K, V>) 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 ; }
由于 jdk8 之后,有两种类型的节点,我们还说过,这两种节点是 爷孙
的关系.
链表的节点遍历就不用看了,比较简单。我们看下 红黑树的节点的查找:
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 final HashMap.TreeNode<K, V> getTreeNode (int h, Object k) { return ((parent != null ) ? root() : this ).find(h, k, null ); } final HashMap.TreeNode<K, V> find (int h, Object k, Class<?> kc) { HashMap.TreeNode<K, V> p = this ; do { int ph, dir; K pk; HashMap.TreeNode<K, V> pl = p.left, pr = p.right, q; if ((ph = p.hash) > h) p = pl; else if (ph < h) p = pr; 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; 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 ; }
这个查找关键因素在于判断对左子树还是右子树进行递归遍历匹配。
以上就是红黑树的查找过程了。
# HashMap 的其他常用方法
这些方法中的实现,我们大部分都分享过了。接下来,我们简单的看下其是如何调用的就好了.
# HashMap.containsKey()
和 HashMap.containsValue()
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 public boolean containsKey (Object key) { return getNode(hash(key), key) != null ; } public boolean containsValue (Object value) { HashMap.Node<K, V>[] tab; V v; if ((tab = table) != null && size > 0 ) { for (int i = 0 ; i < tab.length; ++i) { for (HashMap.Node<K, V> e = tab[i]; e != null ; e = e.next) { if ((v = e.value) == value || (value != null && value.equals(v))) return true ; } } } return false ; }
这里为什么可以通过 .next
的方式去遍历呢?
这个主要是考虑在转换成树节点和树节点的新增的时候。 hashMap
在这两个时刻都对 HashMap
的 next
"指针" 进行了维护。所以,在这里就可以明白了,为什么树化节点时候还要维护 .next
了.
# HashMap.size()
HashMap
的 size
函数最简单了,因为 HashMap
内部本来就维护了一个 size
字段,来记录 HashMap
的元素数量.
1 2 3 4 5 6 7 @Override public int size () { return size; }
# HashMap.isEmpty()
HashMap 的 isEmpty 同样简单,废话少说,看下它的实现
1 2 3 4 5 6 7 @Override public boolean isEmpty () { return size == 0 ; }
# HashMap.entrySet()
这个方法是我们在遍历的时候,常用的一个方法,它的实现有点小复杂。 我们来看下。
1 2 3 4 5 6 7 8 9 10 11 12 13 public Set<Map.Entry<K, V>> entrySet() { Set<Map.Entry<K, V>> es; return (es = entrySet) == null ? (entrySet = new HashMap.EntrySet()) : es; }
从上面的代码可以看出,如果 entrySet
这个属性的为 null
的时候,就会返回一个空的 HashMap
的 entry
, 否则就返回 entrySet(es)
.
那么 entrySet
这个字段是怎么来的呢?
最先 HashMap 定义了一个这样的字段。
1 2 3 4 5 transient Set<Map.Entry<K, V>> entrySet;
好了,我们接着来看 HashMap.EntrySet()
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 72 73 74 75 final class EntrySet extends AbstractSet <Map .Entry <K , V >> { public final int size () { return size; } public final void clear () { HashMap.this .clear(); } public final Iterator<Map.Entry<K, V>> iterator() { return new HashMap.EntryIterator(); } public final boolean contains (Object o) { if (!(o instanceof Map.Entry)) return false ; Map.Entry<?, ?> e = (Map.Entry<?, ?>) o; Object key = e.getKey(); HashMap.Node<K, V> candidate = getNode(hash(key), key); return candidate != null && candidate.equals(e); } public final boolean remove (Object o) { if (o instanceof Map.Entry) { Map.Entry<?, ?> e = (Map.Entry<?, ?>) o; Object key = e.getKey(); Object value = e.getValue(); return removeNode(hash(key), key, value, true , true ) != null ; } return false ; } public final Spliterator<Map.Entry<K, V>> spliterator() { return new HashMap.EntrySpliterator<>(HashMap.this , 0 , -1 , 0 , 0 ); } public final void forEach (Consumer<? super Map.Entry<K, V>> action) { HashMap.Node<K, V>[] tab; if (action == null ) throw new NullPointerException(); if (size > 0 && (tab = table) != null ) { int mc = modCount; for (int i = 0 ; i < tab.length; ++i) { for (HashMap.Node<K, V> e = tab[i]; e != null ; e = e.next) action.accept(e); } if (modCount != mc) throw new ConcurrentModificationException(); } } }
# HashMap.replace()
这个是 HashMap
覆盖掉 JDK8
版本中 的替换方法,将制定的 K-V
映射替换掉。这个方法也会匹配 Value
的值是否相等。
1 2 3 4 5 6 7 8 9 10 11 12 @Override public boolean replace (K key, V oldValue, V newValue) { HashMap.Node<K, V> e; V v; if ((e = getNode(hash(key), key)) != null && ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) { e.value = newValue; afterNodeAccess(e); return true ; } return false ; }
这个也是覆盖 JDK8
版本中 Map
的方法,返回原来的值。
1 2 3 4 5 6 7 8 9 10 11 @Override public V replace (K key, V value) { HashMap.Node<K, V> e; if ((e = getNode(hash(key), key)) != null ) { V oldValue = e.value; e.value = value; afterNodeAccess(e); return oldValue; } return null ; }
# HashMap.forEach()
这个也是一个常用的方法,实现也比较简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void forEach (BiConsumer<? super K, ? super V> action) { HashMap.Node<K, V>[] tab; if (action == null ) throw new NullPointerException(); if (size > 0 && (tab = table) != null ) { int mc = modCount; for (int i = 0 ; i < tab.length; ++i) { for (HashMap.Node<K, V> e = tab[i]; e != null ; e = e.next) action.accept(e.key, e.value); } if (modCount != mc) throw new ConcurrentModificationException(); } }
# HashMap.clear()
清空方法也是很简单的,逐一置空 null
HashMap
的每个 bin
1 2 3 4 5 6 7 8 9 public void clear () { HashMap.Node<K, V>[] tab; modCount++; if ((tab = table) != null && size > 0 ) { size = 0 ; for (int i = 0 ; i < tab.length; ++i) tab[i] = null ; } }
# 问题解答
如果我的 HashMap 的初始大小设置为 [3|9|12]
, 第一次扩容的时候,容量变为了多少?是如何进行扩容的?
(有毒的问题) 假设 Hash 表的长度是 32, 已知某一个 bin 中的链表长度为 7, 如果新增一个元素还是在该 bin 中的时,会进行什么操作? resize
还是 treeifyBin
? 假设完成这个操作后该 bin 中元素数量没变,又新增一个元素还是到该 bin 中,这时进行什么操作?
# 总结
表中允许 null 的键和 null 值。
线程不同步,
不保证元素的顺序。
# 网上常见面试问题汇总以及参考解答
# 冷门知识点
# JDK 变更历史说明
# 课后娱乐
java 实现红黑树
自定义实现 hashMap。
# 参考文档 & 答谢
# 感受
注释:新字段要加注释标注此字段的作用,该字段是什么含义。
阅读之前记录
1. 图解。遇到问题,画图说明。
2. 一定要有自己的理解。
3. 对比其他版本 JDK。
# 最后
期望与你一起遇见更好的自己
扫码或搜索:方家小白
发送 290992
即可立即永久 解锁本站全部文章