HashMap 源码解析(基于 JDK 1.8)
源码环境: JDK 1.8。
本文不介绍红黑树节点的处理过程。
在 1.8 中,HashMap 是数组+链表+红黑树。
1 常用变量及节点类
如图所示,在下面的 HashMap 中,桶数组 table 有着 64 个 Node,大小 size 是 3+1+9 = 13。这个 HashMap 中只有三个桶非空:table[0] 是一个长度为 3 的链表,table[1] 是一个长度为 1 的链表,table[3] 是一个长度为 9 的红黑树。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;// 默认初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;// 默认装载因子
static final int TREEIFY_THRESHOLD = 8;// 桶数组某位置链表节点超过这个数,则从链表转变为红黑树
static final int UNTREEIFY_THRESHOLD = 6;// 调用 split 时,桶数组某位置链表节点不大于这个数,则从红黑树转链表。
static final int MIN_TREEIFY_CAPACITY = 64;// 桶数组容量小于这个数,则不转化为红黑树,而是扩容 resize
transient Node<K,V>[] table;// 桶数组
transient Set<Map.Entry<K,V>> entrySet;// 并不存储数据,可以理解为视图
transient int size;// 键值对的总数
transient int modCount;// 用于快速失败
int threshold;// 阈值,table 初始化后等于 capacity*loadFactor
final float loadFactor;// 装载因子
final int capacity() {
return (table != null) ? table.length :// table 不空则是 table 的长度
(threshold > 0) ? threshold :// 否则是 threshold
DEFAULT_INITIAL_CAPACITY;// 否则是默认容量
}
节点有两种,即 Node 和 TreeNode,下面的类图展示了两者的关系,有两个 Entry,上面的 Entry 是 Map.Entry,下面的是 LinkedHashMap.Entry 。简单地说,TreeNode 也是 Node 的子类,所以 TreeNode 也有 Node 的属性。需要关注的是 Node.next,在后面使用迭代器进行遍历时,并不区分是红黑树还是链表,统一用 next 获得下一个。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> 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;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
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);
}
}
2 构造器及 tableSizeFor
一共四种构造器,无参构造器(以下称为构造器1)会初始化装载因子,含有 initialCapacity 的构造器(以下称为构造器2)调用另一个构造器,含有 initialCapacity 和 loadFactor 的构造器(以下称为构造器3)初始化了 loadFactor,以及 threshold,在 put 时才真正初始化桶数组。含有 Map 的构造器(以下称为构造器4)则初始化 loadFactor 并添加进所有元素。
public HashMap() {
// CAPACITY默认16,装载因子默认0.75,所以默认threshold是12,这些初始化操作在 resize 执行
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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;
this.threshold = tableSizeFor(initialCapacity);// 获取一个2的幂的结果,这里表示的其实是容量,未乘loadFactor。
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
接下来介绍 tableSizeFor(initialCapacity),putMapEntries 放在后面介绍。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
下面假定 cap 为非负数,取值范围是[0, 231-1]。根据代码,可以得到下面的分段函数,y 为返回的结果。
解释下 cap 为其他的具体处理。先计算 int n = cap -1
,cap 分为两种情况,是 2 的幂,或者不是 2 的幂。如果是 2的幂,则 n 的最高位的 1 是 cap 最高位的 1 向右移位;否则,两者最高位的 1 是在同一个位置。
下面进行五次移位以及或操作,最终得到的 n 是:从刚开始算出的 n 的最高位的 1 的位置,后面的每一位都变成1。然后 n+1 返回的正好是 2 的幂。
如图,分别展示了 cap 为 256 和 cap 为 257 的情况。图中每 8 位算一组,填充为橙色的表示 1,否则为 0。第三行的 newN 是算出的结果。
3 put相关方法
3.1 put/putIfAbsent/putAll
put 和 putIfAbsent 的区别在于是否第四个参数,true 表示只有 key 不存在时插入,或者 key 对应的是 null 时覆盖原来的 value;false 表示总是可以插入或者覆盖原来的 key 对应的 value。
putAll 调用了 putMapEntries,第二个参数为 true,表示是放入而不是新建。具体是调用了 afterNodeInsertion,在 HashMap 中无意义。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);// 允许覆盖原来的 key 的 value
}
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
3.2 putMapEntries
该方法被 putAll 和构造器4使用,在构造器中 evict 为 false,在 putAll 中为 true,false 表示是初始构建该 map。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
//首先要找到一个capacity,capacity = s/loadFactor。
//为了避免出现小数,取整变成向下取整,则+1.0f
float ft = ((float)s / loadFactor) + 1.0F;
// 不允许 t 超过 MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// threshold太小则重置threshold,这里threshold代表capacity,在resize()里*loadFactor
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)// table不空但Map的大小大于threshold
resize();//扩容
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);// 逐个插入
}
}
}
3.3 hash
计算 key 的 hash值:如果 key 为 null,则返回0。否则记 h = key.hashCode(),返回 h ^ (h>>>16)。
这个 >>> 表示无符号右移,即将 h 向右移16位,低 16 位都移出去了,高 16 位补 0。
^ 表示异或,对应位置相同(都是1或0)为0,不同为1。
这里进行右移和异或的作用是:考虑到哈希表长度一般都不大,如果只用 hashCode 来寻址,则取模的时候高位的值没有被用上,冲突的概率更大。为了减少冲突,进行上述操作。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3.4 putVal
处理过程:
-
判断数组table[]是否为空或为null,如果是,执行resize()进行初始化;
-
进行下列操作
2.1 由 (n - 1) & hash得到插入的桶数组的位置 ,如果对应的桶为空,则新建节点。
2.2 否则,记 e 为:满足 e.key 和 插入的 key 相同,且在未插入前就存在的 Node,用于处理是否覆盖原来 key 对应的 value 的情况。**
2.2.1 如果桶中的元素对应的 p.key 和 出去的 key 一致,则设置 e = p。**
2.2.2 否则,判断桶中节点是否为红黑树的节点,是则执行红黑树的插入操作。**
2.2.3 否则,当前桶存储的是链表,只需要不断向后查找,如果找到相同的 key,则退出;否则,在末尾插入新节点。**
2.2.3.1 插入后,判断当前桶的链表元素个数是否超过 TREEIFY_THRESHOLD(原来链表中有TREEIFY_THRESHOLD 个,再插入一个,才满足条件),是的话则尝试将链表转换为红黑树(不一定转换,还要满足 MIN_TREEIFY_CAPACITY 的条件)**
2.2.4 如果 e != null,说明相同的 key 出现过了,如果对应的值是 null 或者onlyIfAbsent 为 false,可以覆盖;否则,不能覆盖。返回旧值。
-
++size,如果元素个数大于 threshold,执行扩容 resize。**
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果 table 为空或者是长度为0,要初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果 table 中对应位置 (n - 1) & hash 的桶为空,则新建一个节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// e 为:满足 e.key 和 插入的 key 相同,且在未插入前就存在的 Node。
//用于处理是否覆盖原来 key 对应的 //value 的情况。
Node<K,V> e; K k;
// 该桶中第一个 Node 的 key 和上面的 key 相等,不需要继续查找
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 红黑树的情况
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 链表的情况
else {
for (int binCount = 0; ; ++binCount) {
//遍历到末尾没找到相同的 key,则插入到最后(尾插)。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//插入之前的桶中元素个数是 TREEIFY_THRESHOLD,则尝试转换为红黑树。
//不一定转换,还要满足 MIN_TREEIFY_CAPACITY 的条件。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果找到了,则退出
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果对应的 key 已存在相应的值
if (e != null) {
V oldValue = e.value;
// onlyIfAbsent 为 false 或者旧 value 是 null,则可以替换。
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
++modCount;
// 先 ++size,再比较 threshold,所以是判断插入后的整个 map 中 Node 的个数是否大于 threshold
if (++size > threshold)
resize(); //扩容
afterNodeInsertion(evict);
return null;
}
有几点需要解释:
-
由于 n = tab.length 总是 2 的幂,在正数情况下,总有 hash % n == hash & (n-1),用位运算 & 是为了加快计算。
以 100 % 16 为例,100 省略前面的 0 可以写成二进制形式 110 0100,16 写成二进制形式 10000 100 = (1 * 2^6 + 1 * 2^5 + 0 * 2^4)+ 4 100 % 16 = ((...) + 4 )% 2^4 = 4 % 2^4 = 4 也就是说,100 的二进制只保留后四位,而前面的一定是 16 的倍数,取模会去掉。 保留后四位也就是 & 1111,所以 100 % 16 == 100 & (16-1),其他同理可得。
-
比较元素 p.key 和 key :这里用的是
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
。首先比较 hash 值,相等再比较是否 == 或者 key不为 null 时比较 equals。
2.1 如果 hash 值不同则不可能是相同的 key,不满足条件。先比较 hash 可以较快的排除很多不可能的 key。
2.2 如果两个 key 是同一个,满足条件。 == 通常比 equals 要快。
2.3 否则,
key != null
时,如果key.equals(k)
,也满足条件。如果 key 是 null,要相等 k 也要是 null,这种情况在 2.2 已经处理了。这三种判断是从快到慢的,这样设计是为了加快判断的速度。
-
在上述处理步骤 3.3.1 中,插入后的元素个数是 binCount+2,要求插入后满足
if (binCount + 2 > TREEIFY_THRESHOLD)
,或者是if (binCount + 1 >= TREEIFY_THRESHOLD)
,也就是if (binCount >= TREEIFY_THRESHOLD - 1)
。
3.5 resize
resize 用于三种情况:初始化时,插入后元素个数超过阈值时,treeifyBin 时桶数组的元素个数 < 64时。
处理过程:
-
首先根据旧容量 oldCap 和旧阈值 oldThr 的值进行扩容或者初始化
1.1 如果 oldCap > 0
1.1.1 如果 oldCap >= MAXIMUM_CAPACITY,不能扩容了,只是增大 threshold,然后返回
1.1.2 否则,如果 (newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY,将旧阈值加倍获得新阈值
1.2 否则,如果 oldThr > 0,这种情况是初始化了 threshold,但未初始化 table。这时的 threshold 是 tableSizeFor,是2的幂,将这个值直接赋给newCap。
1.3 否则,说明应该是空参构造器,没有初始化 threshold 和 table,在这里计算 newCap 和 newThr。
-
判断 newThr 是否为 0,对应于 1.1 中 1.1.1 和 1.1.2 都不满足的情况和 1.2 的情况。这时候需要计算出一个新的 newThr,由
ft = (float)newCap * loadFactor
计算出一个 ft。如果 newCap 和 ft 都小于 MAXIMUM_CAPACITY,则 ft 取整,否则是 Integer.MAX_VALUE。 -
构建新桶 newTab,将其置为 table,如果原桶数组 oldTab 不空,则遍历里面所有的桶。在处理某个桶的时候,如果这个桶不空,则进行处理,将这个桶中的元素都放到新桶数组的对应位置,记这个桶为 oldTab[j]。
3.1 如果这个桶只有一个元素,则将元素 e 放在 newTab 的 e.hash & (newCap - 1)] 位置。
3.2 如果这个桶对应一个红黑树,按照红黑树的方法 split
3.3 如果这个桶对应一个链表,遍历链表中每一个元素,并根据 e.hash & oldCap 是否为0将这个元素插入 lo 或者 hi 链表后面。遍历完所有元素之后,lo 不空则放入 newTab[j],hi 不空则放入newTab[j+oldCap]。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;// 旧容量
int oldThr = threshold;// 旧阈值
int newCap, newThr = 0;// 新容量和新阈值
// table非空的情况
if (oldCap > 0) {
// oldCap太大,不能再扩容了,增大 threshold 并返回。
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// newCap也小于 MAXIMUM_CAPACITY 且 oldCap 大于 16
// 将阈值也扩容
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 含参数构造器(2,3,4)初始化了threshold,但没有初始化 table
// 此时 threshold 没乘装载因子,所以赋给 newCap
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);
}
// 对应上面的两种的情况,会重新计算newThr
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"})
// 新建 hash 桶数组,并赋给 table。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 原来的桶数组不空,要把原来桶数组的 Node 重新计算位置后放入新的桶数组对应的位置。
if (oldTab != null) {
// 遍历 oldCap 中每个桶
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 如果桶不空
if ((e = oldTab[j]) != null) {
// 将旧桶置空
oldTab[j] = null;
// 如果只有一个元素,将该元素直接放入newTab的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e; //新位置
// 如果是红黑树的情况
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 否则,是链表的情况,具体是将该链表拆分成两个链表lo和hi,放入newTab对应位置
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;
// 构建低位置的链表lo,使用尾插法
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// // 构建高位置的链表ho,使用尾插法
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//lo不空则放在newTab和oldTab同样的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//hi不空则在newTab的新位置(即newTab[j+oldCap],其中j为oldCap中旧位置)
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
这里解释一下链表拆分即(3.3) 的原理:两个元素 e1 和 e2,oldCap = 28,newCap = 29,观察变化情况。
主要分析两点,
-
e.hash & (newCap -1) 和 e.hash & (oldCap -1)的关系:
前者是 Node e在新表的位置,后者是 e 在旧表的位置。
有两种情况:
1.1 位置不变,即在链表 lo
e.hash & (newCap -1)= e.hash & (oldCap -1)
1.2 位置变成原来的位置加上 oldCap,即在链表 hi
e.hash & (newCap -1)= (e.hash & (oldCap -1)) + oldCap
具体是哪一种情况在于多出来的最高位 1 异或的结果是0还是1。
-
使用 e.hash & oldCap == 0 的意义:
如果 e.hash & oldCap == 0,则对应 1.1的情况;否则,对应 1.2 的情况。
e1.hash = xxxxxxxx xxxxxxxx xxxxxxx0 10000001 (1)
e2.hash = xxxxxxxx xxxxxxxx xxxxxxx1 10000001 (2)
oldCap - 1 = 00000000 00000000 00000000 11111111 (3)
newCap - 1 = 00000000 00000000 00000001 11111111 (4)
oldCap = 00000000 00000000 00000001 00000000 (5)
(1) & (3) = 00000000 00000000 00000000 10000001 (6)
(2) & (3) = 00000000 00000000 00000000 10000001 (7)
(1) & (4) = 00000000 00000000 00000000 10000001 (8)
(2) & (4) = 00000000 00000000 00000001 10000001 (9)
(1) & (5) = 00000000 00000000 00000000 00000000 (10)
(2) & (5) = 00000000 00000000 00000001 00000000 (11)
由于 (6) == (7),则 e1 和 e2 在旧桶数组 oldTab 的同一个位置,即 oldTab[129]。
由于 (8) != (9),则 两个元素在新桶数组 newTab 中不在同一个位置,e1 在 newTab[129],
e2 在 newTab[129+oldCap]。
注意到 (10) != (11),newCap - 1 的最高位正好对应着 oldCap,想让这一位和 e.hash 做 &,也就是直接
e.hash & oldCap。如果这一位是0,即 e1 的情况,在低的位置 newTab[129];如果这一位是1,即 e2 的情况,
在高的位置 newTab[129+oldCap]
在扩容后,新位置是 e.hash & (newCap -1),注意观察,由于 oldCap 和 newCap 都是2的幂,newCap - 1 比
起 oldCap - 1 多出一位,在进行 & 时对于 e 需要多考虑一位,区别只在于这一位,有两种情况:如果新增加的这一
位的位置在 e.hash 的对应位置是0,则 e.hash & (newCap -1) == e.hash & (oldCap -1);如果新增加的这
一位是对应的是1,则 e.hash & (newCap -1) == (e.hash & (oldCap -1) + oldCap)。
若 e.hash & oldCap ==0,说明 e.hash 在 (newCap -1) 的最高位的位置是0,所以 e.hash & (newCap-1)
在这个最高位会得到 0,也就是保留在原来的位置 newTab[j];否则,说明 e.hash 在 (newCap -1) 的最高位的位
置是1, e.hash & (newCap-1) 在这个最高位会得到 0,也就是移动到新位置 newTab[j+oldCap]
4 get 相关
4.1 get/getOrDefault
两者都调用了 getNode。
对于 get 如果 getNode 是 null 则返回 null;对于 getOrDefault 如果 getNode 是 null 则返回 defaultValue。
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;
}
4.2 getNode
处理过程:
-
如果 table 已经初始化,table 长度大于0,table[(n-1)&hash] 中第一项也不为空,进行下面的操作:
1.1 先判断第一个元素是不是要找的;
1.2 如果第一个元素的后一个不为空
1.2.1 如果是红黑树,按红黑树的方式查找
1.2.2 如果是链表,则遍历查找
-
不满足条件,直接返回 null。
这里判断两个 key 是否相等的逻辑和 putVal 中的一样,不再解释。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 先判断 1. 是否成立
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 先检查第一个元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果第一个元素的下一个非 null
if ((e = first.next) != null) {
// 如果是红黑树节点
if (first instanceof TreeNode)
return ((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;
}
5 remove 相关
5.1 remove
remove 有两个方法:对于只给定 key 的方法,删除时只比较 key 就可以;对于 给定 key 和 value 的方法,要求 map 中的 key 正好对应相等的 value 才可以删除。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
5.2 removeNode
处理过程:
-
如果 table 已经初始化,table 长度大于0,table[(n-1)&hash] 中第一项也不为空,进行下面的操作:
1.1 先判断第一个元素的 key 是否相等;
1.2 否则,如果第一个元素的后一个不为空
1.2.1 如果是红黑树,按红黑树的方式查找相等的key
1.2.2 如果是链表,则遍历查找相等的key
1.3 根据 matchValue 确定是否需要相同的 value才删除,如果删除
1.3.1 如果是红黑树节点,按照红黑树的方法删除
1.3.2 如果是桶数组在这个位置的桶的第一个元素,则将这个元素的下一个放在桶中。
1.3.3 如果是桶数组中非第一个元素,直接删掉就行。
-
不满足条件,直接返回 null。
//第四个参数 matchValue 表示是否要求 value 相等才能删除,如果是 true,则 value 必须相等。
//第五个参数 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;
// 先判断 1 是否成立
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//使用 node 记录可能出现的key相等的元素,初始为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
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;
}
// 如果在某一步 break,则 p 是 node 的前一个元素
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;
}
解释一下 1.3 中的判断if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v))))
:
- 首先要满足能找到 key 相同的节点,也就是 node != null
- 如果 matchValue 为 false,则不需要比较 value,也就是 ! matchValue
- 如果 matchValue 为 true,需要比较 value,也就是(v = node.value) == value ||(value != null && value.equals(v)),和 putVal 中比较 key 是同样的逻辑,不再解释。
- 上面两个满足或关系,用 ||来连接。
6 replace
两参数的 replace 首先找到对应 Node e,直接 e.value = value。
三参数的 replace 也是找到对应的节点 Node e,但要求 e.value 和 oldValue 相等才可以替换。
replaceAll 是将每个节点 e 的 value 都替换为 function.apply(e.key, e.value),注意这里没有区分红黑树和链表,统一用 next 查找同一个桶中的下一个。
public V replace(K key, V value) {
Node<K,V> e;
//根据 key 找到对应的节点 e
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
// 替换
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
// 根据 key 找到对应的节点 e,&&后面要求 e.value 和 oldValue 相等。
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;
}
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Node<K,V>[] tab;
if (function == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
// 对于一个桶中的元素,统一用 next 查找下一个
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
e.value = function.apply(e.key, e.value);
}
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
7 钩子函数
在 HashMap 中无意义,在 LinkedHashMap 中使用,可以关注我,看我后续关于 LinkedHashMap的文章。
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
8 键值对集合
KeySet 和 EntrySet 继承了 AbstractSet,Values 继承了 AbstractCollection。
三个类的 forEach 类似,都是遍历桶数组中每一个桶,对于每个桶,不管是红黑树还是链表,都是用 next 获取桶中下一个元素。
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }//containsKey
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
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) {
// 使用 next 获取下一个元素
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<V> iterator() { return new ValueIterator(); }
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super V> action) {
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) {
// 使用 next 获取下一个元素
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
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 EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
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 EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
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) {
// 使用 next 获取下一个元素
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
KeySet 和 EntrySet 的 contains 用的都是 getNode,remove 都是 removeNode。
Values 的 contains 内部是遍历查找。
public boolean containsKey(Object key) { // 用于 KeySet
return getNode(hash(key), key) != null;
}
public boolean containsValue(Object value) {// 用于Values
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (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;
}
// 下面为 Node 中的 equals 方法,用于 EntrySet 的contains。
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;
}
9 迭代器
三个迭代器 KeyIterator、ValueIterator 和 EntryIterator 都是继承了 HashIterator,重点在于 HashNode 的nextNode 方法。
其实 nextNode 和上面的 forEach 逻辑基本一致,都是遍历桶数组中的每一个桶,并对当前的桶不断使用 next 获取下一个,直到当前桶没有元素为止。
在后面重写了 nextNode 方法。
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry 用于删除
int expectedModCount; // for fast-fail
int index; // current slot 当前 Node 在桶数组的哪一个桶的位置
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry 遍历,找到第一个非空元素
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
下面重写了 nextNode 方法,意思不变,方便理解,称为 nextNode2,修改最后一个 if,将其注释掉,新加的代码用一个{}放在一个代码块中。
解释一下:
在原来方法的第二行,将 next 赋给 e,在修改的部分又将 e 赋给current,这两句合起来就是 current = next ; 然后 next 取下一个。如果 next 为 null,则获取当前的表 table,未越界,则获取接下来的 t[index],直到找到一个位置满足 t[index] != null,这个位置对应的桶的第一个元素就是 next。
final Node<K,V> nextNode2() {
Node<K,V>[] t;
Node<K,V> e = next; // 首先将 next 赋给 e。
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//if ((next = (current = e).next) == null && (t = table) != null) {
// do {} while (index < t.length && (next = t[index++]) == null);
//}
{ //新添加的代码块
current = e;
next = current.next;// next 取下一个
if (next == null){// 如果为 null
t = table;
if (t != null){
while (index < t.length){
next = t[index];// 尝试获取 t[index] 处的元素
index++;
if (next != null){// 如果是 null,继续循环查找,否则可以退出循环并返回。
break;
}
}
}
}
} //新添加的代码块
return e;
}
下面是我的公众号,Java与大数据进阶,分享 Java 与大数据笔面试干货,欢迎关注
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 用 .NET NativeAOT 构建完全 distroless 的静态链接应用
· 为什么构造函数需要尽可能的简单
· 探秘 MySQL 索引底层原理,解锁数据库优化的关键密码(下)
· 大模型 Token 究竟是啥:图解大模型Token
· 35岁程序员的中年求职记:四次碰壁后的深度反思
· 基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程
· 电商平台中订单未支付过期如何实现自动关单?
· 用 .NET NativeAOT 构建完全 distroless 的静态链接应用
· 上周热点回顾(3.31-4.6)
· 爆肝 1 周,为我的白板工具支持了 mermaid 流程图,为 ai 生成流程图铺平道路