Java 集合源码解析系列:
- 拆行解码 Java 集合源码之总览
- 拆行解码 Java 集合源码之 Collection 的三大体系
- 拆行解码 Java 集合源码之迭代器
- 拆行解码 Java 集合源码之 ArrayList
- 拆行解码 Java 集合源码之 LinkedList
- 拆行解码 Java 集合源码之 HashMap
- 拆行解码 Java 集合源码之 Hashtable
- 拆行解码 Java 集合源码之 LinkedHashMap
- 拆行解码 Java 集合源码之 PriorityQueue
- 拆行解码 Java 集合源码之 ArrayDeque
特性
-
Key、Value 允许为 null。
-
希望 key 是不可变对象, 即 hashcode 不变。
-
不同步,线程不安全。同步使用包装类:
Collections.synchronizedMap
。 -
假设 Hash 函数将元素正确分散在 bucket 中,则此实现为基本操作(get 和 put)提供常量时间的性能。
-
视图上的迭代所需的时间与 HashMap 实例的“容量”(table[].length)及其大小(键-值对数量)成正比。
迭代性能很重要,则不要将初始容量设置得过高(或负载因数过低)。
-
重要且影响性能的参数:初始容量和负载因子。
-
容量是哈希表中存储桶的数量,初始容量只是创建哈希表时的容量。
-
负载因子是在自动增加哈希表容量之前允许哈希表元素与全部容量的占比。
-
当哈希表中的键值对数超过负载因子和当前容量的乘积时,哈希表将被重新哈希(即,内部数据结构将被重建),容量为原来的两倍。
默认负载因子(.75)在时间和空间成本之间提供了一个很好的权衡。
太大减少空间,但是查找性能差;太小反之,但是频繁 rehash 影响性能。
-
初始化时,应该合理设置容量和负载因子,尤其可预估元素数量时,避免后续的 rehash。
-
-
上下阈值,控制链表(Node)和红黑树(TreeNode)的转换。
在检索链表或红黑树时,会根据头结点类型去执行不同的查找逻辑。
在树化的情况下,,一样支持遍历,甚至在结点较多时, 查找性能更快。 但是一般情况下不会转为树,每次检查结点类型就给链表的查找带来了额外消耗。
-
由于LinkedHashMap子类的存在,普通模式与树模式之间的使用和转换变得复杂。
钩子方法定义为在插入、删除和访问时调用,允许LinkedHashMap内部保持独立于这些机制。 模板方法,子类可能对结点做了扩展(存前后结点),,允许转换的时候对转化后的结点进行补充。
结点数据结构
链表 Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //用来定位数组索引位置
final K key;
V value;
Node<K,V> next; //链表的下一个node
Node(int hash, K key, V value, Node<K,V> next) { ... }
public final K getKey(){ ... }
public final V getValue() { ... }
public final String toString() { ... }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
红黑树Node
LinkedHashMap:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
// 左 <= 父 <= 右
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
// 和 next 就构成了树节点上的双向链表,方便遍历
// 初始树化时,prev 和 next 就是原链表上的顺序
// put 时的维护方式和链表是一样的。连接到最底部结点(叶子节点或只有一边子树)的左或者右,
// 父节点就是新节点的 prev,父节点的 next 变成新节点的 next。
// 这个时候的顺序就不再是插入顺序了。
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; // 标记结点颜色
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
final TreeNode<K,V> root() { // 返回根结点
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
源码
预设参数一览
-
初始容量:16
-
容量:2 的 n 次幂 <= (1 << 30)
-
默认负载因子:0.75
-
树化阈值:大于 2,至少为 8
-
解除树化阈值:6
-
树化的前提条件,table[] 的容量:>= 64(4 倍的树化阈值)。
如果不达到 64,树化阈值达到了,只会去扩容。而不是树化。
为了避免树化和扩容的冲突。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 2 的 n 次幂一定小于等于 1 << 30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 树化的前提条件,table[] 的容量:>= 64(4 倍的树化阈值)。
* 如果不达到 64,树化阈值达到了,只会去扩容。而不是树化。
* 为了避免树化和扩容的冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
相关参数一览
/**
* 容量一定是 2 的 n 次幂
*/
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
/**
* 扩容时机的阈值
* 如果尚未分配表数组,则此字段将保留初始数组容量,或者为零,表示DEFAULT_INITIAL_CAPACITY。
*/
int threshold;
/**
* The load factor for the hash table.
*/
final float loadFactor;
构造函数
确保容量是 2 的 n 次幂,就要控制住最大的输入口子:构造函数(内部扩容,只要保证初始是 2 的 n 次幂,后面翻倍扩容就没问题)。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
// 控制最大容量
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 确保初始容量为仅大于等于 initialCapacity 的 2 的 n次幂
// table[] 是懒加载的。未分配之前,threshold = capacity,暂存一下。
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 转移键值对
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 因为不仅用于构造函数, 还用于 putAll。所以相当于两个情况
if (table == null) { // 计算预估大小来初始化
float ft = ((float)s / loadFactor) + 1.0F; // 任何场景都推荐通过"预估容量/负载因子 + 1"来避免频繁 rehash
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold) // 这个情况下, threshold 就是预设容量
// 确保初始容量为仅大于等于 initialCapacity 的 2 的 n次幂
threshold = tableSizeFor(t);
}
else if (s > threshold)
// 已初始化了,并且数量大于阈值。这种情况属于预先扩大容量,再put元素
// 为啥不是原数量 + 新数量 > threshold ? 因为可能会有重复, 不能完全根据两者相加判断
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// 这里可能还是会 resize
putVal(hash(key), key, value, false, evict);
}
}
}
/**
* 返回刚好大于等于指定容量的 2 的 n 次幂
* public static int numberOfLeadingZeros(int i) {
* // HD, Count leading 0's
* if (i <= 0)
* return i == 0 ? 32 : 0;
* // 大于 0 的话, 最多也只有高 31 位可能都为为 0, 即 0 的数量最多 31
* int n = 31;
* // 以下过程其实相当于二分查找
* // 对高 16 位判断。如果有值,则将高 16 移至低 16。
* // 最高位肯定在前 16 上,所以 0 的数量最大值 - 16
* if (i >= 1 << 16) { n -= 16; i >>>= 16; }
* // 对低 16 位的高 8 位判断。如果有值,则将高 8 移至低 8。0 的数量最大值 - 8
* if (i >= 1 << 8) { n -= 8; i >>>= 8; }
* // 对低 8 位的高 4 位判断。如果有值,则将高 4 移至低 4。 0 的数量最大值 - 4
* if (i >= 1 << 4) { n -= 4; i >>>= 4; }
* // 对低 4 位的高 2 位判断。如果有值,则将高 2 移至低 2。0 的数量最大值 - 2
* if (i >= 1 << 2) { n -= 2; i >>>= 2; }
* // 清除最后一位的影响. 因为最多本来只有 31
* return n - (i >>> 1);
* }
*/
static final int tableSizeFor(int cap) {
// numberOfLeadingZeros 返回无符号数前面 0 的个数
// -1 >>>: 相当于把 1 从最高位右移到原值二进制的前一位上.
// 比如说 cap = 32, cap - 1 = 31(11111); 前面 0 的个数是 27
// -1 >>> 27 = 11111 + 1 = 32
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
Hash 函数
HashMap 每次添加键值对时,会根据 key 的 hash code 计算键值对在 table[] 中的索引位置。
而这个过程需要两步:
- hashcode 经过 hash 函数获得一个 hash 值;(利用扰动函数尽量避免冲突)
1.8 及以后:1 次位移、一次异或
static final int hash(Object key) {
int h;
// 高位和低位异或,充分利用 key 本身的特性来减少 hash 冲突。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.7 及之前:4次位移 + 5次异或运算。
h = k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
其实1.8 这样的次数可以有足够降低碰撞几率。再多的运算,由于边际效益,相当于 80 % 只提高了 20 % 的效益,着实没有太大必要。
- hash 值与 table[] 长度取余获得索引值。
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length-1); // 取模运算,等同于 h%length
}
可以通过二进制运算,快速得到索引值。这也是为什么要求数组长度必须是 2 的 n 次幂的原因。
图片来源:(tech.meituan.com/2016/06/24/…
Put
put 元素,包括 putAll(putMapEntries)、put、putIfAbsent 都是通过直接或循环调用 putVal方法来添加键值对。
因此 put 的逻辑重点关注 putVal 方法;其他也有类似添加键值对的方法,但是都是类似的,都可能涉及到下面全部或部分的逻辑判断。
先思考一下,put 一个键值对会经历怎么样的流程?
- 计算 key 的 hash;
- 计算数组索引;
- 位置是否存在节点;
- 存在节点是链表,还是树。
- 是否存在相同的 key:添加还是替换;
- 是否需要扩容;
- 是否需要树化;
- 是否需要解除树化;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; 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 {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// key 已存在, 走后面替换流程
e = p;
else if (p instanceof TreeNode)
// 是树节点, 走树节点插入:涉及比较、平衡
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 遍历当前链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 尾插法
// 1.7 是放在链头的, 即直接放在 table[i]
p.next = newNode(hash, key, value, null);
// -1 是算添加后的长度: binCount + 1 >= TREEIFY_THRESHOLD
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 树化
treeifyBin(tab, hash);
break;
}
// 遍历过程中, 发现 key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// p = p.next
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 为空, 或不限制不存在的条件
// 为了处理 putIfAbsent
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 改变结点后, 留给子类的钩子方法
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 添加后的数量 > 阈值, 则扩容
if (++size > threshold)
resize();
// 新增结点后, 留给子类的钩子方法
afterNodeInsertion(evict);
return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 只有容量 >= 64 才树化, 否则扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
// 遍历链表, 替换成树节点
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);
}
}
Class TreeNode {
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 树在迭代器中删除结点,在数组上的不一定是根节点。
// 所以需要重新获取。
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// 优先根据 hash 判断大小
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// key 已存在, 直接返回
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 相同, 则根据 Comparable 比较
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 类不可比较(有可能没指定泛型, 有多种类型在), 或比完是一样的
if (!searched) {
TreeNode<K,V> q, ch;
// 这意思是, hash相同情况下, 这种无法比较都扔在一个子树下面了? 所以全部遍历一遍来找。
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;
}
// 类型不一样呗, 根据类型的 hashcode 大致比下大小
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
// 找到最下层的结点(无子结点或只有一个)
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
// 小放左边
xp.left = x;
else
// 大在右边
xp.right = x;
// 父.next = cur
xp.next = x;
// prev = 父节点
x.parent = x.prev = xp;
// next = 父.原next
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 平衡红黑树, 将根节点放在 table[i]
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
// 都是先比 hash, 再比 Comparable, 再比 Class
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (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 (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);
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;
}
}
}
}
// 将根节点放在 table[i]
moveRootToFront(tab, root);
}
}
扩容 resize方法
注意,下面两个步骤都在 resize 方法里面,只是拆开分析而已。
容量、阈值计算
扩容对于容量和扩容阈值的计算稍微有点说法,简单来说,就是控制住[“默认”(16),最大(1 << 30)]的区间。
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; // double threshold
}
// 还未初始化, 初始容量就放在 threshold, 根据这个初始化
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 无参构造
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// 进入该if有两种可能
// 第一:进入此“if (oldCap > 0)”中且不满足该if中的两个if:说明是刚好扩容到最大容量(阈值翻倍也有可能会超过最大容量) 或 旧容量小于16(原容量极小, 导致原来的阈值可能为 0?)
// 第二:进入这个“else if (oldThr > 0)”:说明是在第一次put
float ft = (float)newCap * loadFactor;
// 最大容量, 而且负载因子较大, 导致计算的阈值也超过最大容量
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
结点的重新索引
先决条件是 oldTab != null
,所以数组初始化走不到这一步的。
而后,应该依次遍历 oldTab 的每个位置,重新计算该位置下的单个结点,或遍历链表和树来计算每个结点。
有个有趣的地方是,当数组是翻倍扩容时,新结点的 hash & (newCap - 1)
计算出来的结果:
- 要么与原来计算的结果一直 index =
hash & (oldCap.length - 1)
- 要么就是原来的结果 + 旧容量的位置:
index + oldCap
图源来自 (tech.meituan.com/2016/06/24/…
所以,从上图可以看到,刚好是所增加 oldCap 的二进制和 Node 的 hash 与运算的结果是否为 1,来判断索引是不变还是 + oldCap。
如此,代码中就不需要重新计算 hash & (newCap - 1)
,可以通过 hash & oldCap == 0
判断并快速得到索引。
然后,单个 bit 上的值是0 或 1,概率学上是五五开。这样又可以均匀拆分链表。
最终呈现的结果是这样的(以 16 扩充为 32 举例):
图源来自 (tech.meituan.com/2016/06/24/…
JDK 7 及之前,采用的是头插法。当结点的索引值一致时,是直接把结点放在数组上。所以一开始 put 的结点,最后在链表的末尾。
而扩容后, 链表会倒置。
JDK 8 及以后,使用尾插法,把结点添加在链表的末尾。扩容后,迁移后的结点顺序是没变化的。
同时也避免了多线程扩容时链表成环的情况。
if (oldTab != null) {
// 遍历数组上的头结点
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 没有结点, 没有树, 没有链表, 就跳过
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 只有头结点, 那直接重新计算索引就可以了(不适用 hash & oldCap, 是避免就一个结点的情况, 还有if..else..的分支)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 分割树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 链表结点的顺序不变
// 原链表的头尾(newTab[j])
Node<K,V> loHead = null, loTail = null;
// 新链表的头尾(newTab[j + oldCap])
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;
}
}
}
}
}
Class TreeNode {
/**
* @param tab newTab
* @param index j
* @param bit oldCap
* 将树箱中的节点拆分为较高和较低的树箱,如果现在太小,则取消树化。 仅从调整大小调用
*/
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;
// 同链表的逻辑, 只是需要额外计算个数(是否退树化)
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)
// 个数 <= 6,去树化
// 因为上面已经维护好链表, 只需把结点类型替换, 去除树结构即可
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
// 如果 hiHead 为空, 就说明没有结点退出树结构, 啥也不变
if (hiHead != null) // (else is already treeified)
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);
}
}
}
}
Get & Remove
Remove
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* @param matchValue 如果为 true, 则需要匹配 Value 是否一致
* @param movable 如果为 false, 则不移动其他结点
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 从查找结点这一点上,是一模一样的。
// 优先头结点,要么遍历树,要么遍历链表
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
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 TreeNode)
// 遍历树
node = ((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 TreeNode)
((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;
}
钩子方法
钩子方法是让子类扩展,去维护自己的结点结构的。
// 结点数据访问、改变后,常用于 put、replace、compute、merge
void afterNodeAccess(Node<K,V> p) { }
// 结点插入后,常用于 put、computeIfAbsent、compute、merge
void afterNodeInsertion(boolean evict) { }
// 结点删除后,用 removeNode
void afterNodeRemoval(Node<K,V> p) { }