HashMap原理分析及源码分析
1. HashMap简介
哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表。
在讨论哈希表之前,我们先大概了解下其他数据结构在新增,查找等基础操作执行性能:
- 数组:
采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n)。当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。 - 线性链表:
对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)。 - 二叉树:
对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。 - 哈希表:
相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。
接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的:
我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。比如我们要新增或查找某个元素,我们通过把当前元素的关键字,通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。
- 哈希冲突(哈希碰撞)
如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀。但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表+红黑树的方式。
1.1 hashMap数据结构
- 链表Node节点的定义:
/** HashMap的节点类型。既是HashMap底层数组的组成元素,又是每个单向链表的组成元素 */
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //key的哈希值
final K key;
V value;
Node<K,V> next; //指向下个节点的引用
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
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;
}
}
1.2. jdk 7 与 jdk 8 中关于HashMap的对比
1、JDK 8 之前:
JDK 8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
当 HashMap 中有大量的元素都存放到同一个桶(bucket)中时,这个桶下有一条长长的链表,极端情况HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
2、JDK 8:
DK7与JDK8中HashMap实现的最大区别就是对于冲突的处理方法。JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn)),用数组+链表+红黑树的结构来优化这个问题。
- 对比:
- (1)JDK 8时数据结构是红黑树+链表+数组的形式,当桶内元素大于8时,便会树化
- (2)hash值的计算方式不同,参考: Map中的hash()分析.
- (3)JDK 1.7 table在创建hashmap时分配空间,而JDK 1.8在put的时候分配,如果table为空,则为table分配空间。
- (4)在发生冲突,插入链表数据时,JDK1.7 是头插法,JDK1.8是尾插法。
- (5)在resize扩容操作中,JDK1.7需要重新进行index的计算,而JDK1.8不需要,通过判断相应的位是0还是1,要么依旧是原index,要么是oldCap + 原index.
2. 源码分析
2.1 静态变量
1、table默认初始化容量 16。容量必须为2的次方。默认的hashmap大小为16.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ;
2、table最大的容量大小2^30,约10亿
static final int MAXIMUM_CAPACITY = 1 << 30
3、默认负载因子大小为0.75,即实际数量超过总数DEFAULT_LOAD_FACTOR的数量即会发生resize动作。
static final float DEFAULT_LOAD_FACTOR = 0,75f:
为什么是0.75,网上有些答案说是,因为capcity是2的次方,那么与之相乘会得到整数。还有一种说法更为可靠,负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
4、 触发树化的阈值1:将链表转化为红黑树的临界值。当添加一个元素被添加到有至少TREEIFY_THRESHOLD个节点的桶中,桶中链表将被转化为树形结构。 临界值最小为8。
static final int TREEIFY_THRESHOLD = 8;
5、触发树化的阈值2:桶可能被转化为树形结构的最小容量。当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采取扩容来尝试减少冲突。
static final int MIN_TREEIFY_CAPACITY = 64;
- 触发树化的条件:单个链表的容量超过阈值8,且此时table的长度大于64
6、恢复成链式结构的桶大小临界值。当resize后或者删除操作后,单个链表的容量低于阈值时,将红黑树转化为链表。
static final int UNTREEIFY_THRESHOLD = 6;
2.2 成员变量
1、node数组
transient Node<K,V>[] table;
2、集合的键值对的个数
transient int size;
3、hashMap被结构化修改的次数,结构化修改指的是成功添加和删除数据引起的结构修改,排除覆盖和扩容(rehash)
transient int modCount;
4、扩容的阈值:hashMap的元素个数大于阈值时,需要进行扩容。
计算公式:负载因子容量
默认为:0.75 16 = 12
int threshold;
5、负载因子:默认为0.75,一般不建议改
final float loadFactor;
2.3 构造方法
1、无参构造
public HashMapp(){
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
2、指定初始容量和负载因子:
public HashMapp(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);
}
3、指定初始容量:
public HashMapp(int initialCapacity){
this(initialCapacity,DEFAULT_LOAD_FACTOR);
}
- 一般用于确定map的元素个数时,减少扩容次数
2.4 get方法分析
/**返回指定的key映射的value,如果value为null,则返回null。
* @see #put(Object, Object) */
public V get(Object key) {
Node<K,V> e;
//如果通过key获取到的node为null,则返回null,否则返回node的value。getNode方法的实现就在下面。
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
从源码中可以看到,get(E e)可以分为三个步骤:
1、通过hash(Object key)方法计算key的哈希值hash。
2、通过getNode( int hash, Object key)方法获取node。
3、如果node为null,返回null,否则返回node.value。
2.3.1 hash(Object key)方法
HashMap的数据是存储在链表数组里面的。在对HashMap进行加、删除、查找等操作时,都需要根据K-V对的键值定位到他应该保存在数组的哪个下标中。而这个通过键值求取下标的操作就叫做哈希。
求哈希简单的做法是先求取出键值的hashcode,然后在将hashcode得到的int值对数组长度进行取模。为了考虑性能,Java总采用按位与操作实现取模操作。
通过hash(Object key)方法计算key的哈希值hash,方法又可分为三步:
(1)取key的hashCode第二步
(2)key的hashCode高16位异或低16位,代码实现如下:
/**通过键的hashCode, 计算key的哈希值。 */
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(3)将第一步和第二部得到的结果进行取模运算,得到在哈希桶数组的位置。计算位置的方法如下:
//其中的n为数组的长度,hash为hash(key)计算得到的值
(n - 1) & hash
- 用这个取余运算可以保证算出来的索引在数组大小范围内,不会超出。
- Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。
代码实现如下:
//根据hash值和数组长度算出索引值
static int indexFor(int h,int length){
return h & (length-1);
}
2.3.2 getNode( int hash, Object key)
getNode方法又可分为以下几个步骤:
1、如果哈希表为空,或key对应的桶为空,返回null
2、如果桶中的第一个节点就和指定参数hash和key匹配上了,返回这个节点。
3、如果桶中的第一个节点没有匹配上,而且有后续节点:
(1)如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
(2)如果当前的桶不采用红黑树,即桶中节点结构为链式结构,遍历链表,直到key匹配
4、找到节点返回Node,否则返回null。
代码实现如下:
/**根据key的哈希值和key获取对应的节点
* @param hash 指定参数key的哈希值
* @param key 指定参数key
* @return 返回node,如果没有则返回null
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//如果哈希表不为空,而且key对应的桶上不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果桶中的第一个节点就和指定参数hash和key匹配上了
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//返回桶中的第一个节点
return first;
//如果桶中的第一个节点没有匹配上,而且有后续节点
if ((e = first.next) != null) {
//如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果当前的桶不采用红黑树,即桶中节点结构为链式结构
do {
//遍历链表,直到key匹配
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//如果哈希表为空,或者没有找到节点,返回null
return null;
}
2.5 put方法分析
2.5.1 hashMap存储原理分析
举例说明:
通过图形表示为:
即put 操作的主要流程如下:
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
2.5.2 put方法实现
/** 将指定参数key和指定参数value插入map中,如果key已经存在,那就替换key对应的value
* @param key 指定key
* @param value 指定value
* @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
*/
public V put(K key, V value) {
//putVal方法的实现就在下面
return putVal(hash(key), key, value, false, true);
}
put(K key, V value)可以分为三个步骤:
1、通过hash(Object key)方法计算key的哈希值。
2、通过putVal(hash(key), key, value, false, true)方法实现功能。
3、返回putVal方法返回的结果。
哈希值是如何计算的上面已经写了。下面看看putVal方法是如何实现的。
2.5.3 putVal方法
/**Map.put和其他相关方法的实现需要的方法
* @param hash 指定参数key的哈希值
* @param key 指定参数key
* @param value 指定参数value
* @param onlyIfAbsent 如果为true,即使指定参数key在map中已经存在,也不会替换value
* @param evict 如果为false,数组table在创建模式中
* @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果哈希表为空,调用resize()创建一个哈希表,并用变量n记录哈希表长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果指定参数hash在表中没有对应的桶,即为没有碰撞
if ((p = tab[i = (n - 1) & hash]) == null)
//直接将键值对插入到map中即可
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))))
//将桶中的第一个节点记录起来
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) {
//在链表尾部插入键值对
p.next = newNode(hash, key, value, null);
//如果链的长度大于TREEIFY_THRESHOLD这个临界值,则把链变为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//跳出循环
break;
}
//如果找到了重复的key,判断链表中结点的key值与插入的元素的key值是否相等,如果相等,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
//如果key映射的节点不为null
if (e != null) { // existing mapping for key
//记录节点的vlaue
V oldValue = e.value;
//如果onlyIfAbsent为false,或者oldValue为null
if (!onlyIfAbsent || oldValue == null)
//替换value
e.value = value;
//访问后回调
afterNodeAccess(e);
//返回节点的旧值
return oldValue;
}
}
//结构型修改次数+1
++modCount;
//判断是否需要扩容
if (++size > threshold)
resize();
//插入后回调
afterNodeInsertion(evict);
return null;
}
2.6 resize方法分析
1、为什么要进行resize扩容操作?
向hashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,hashMap就需要扩大数组的长度,以便能装入更多的元素。当然数组是无法自动扩容的,扩容方法使用一个新的数组代替已有的容量小的数组。
扩容的目的是减少hash碰撞,让key更加散列,从而提高hashmap的查询效率。
2、 在什么时候需要进行扩容?
resize扩容操作主要用在两处:
(1)向一个空的HashMap中执行put操作时,会调用resize()进行初始化,要么默认初始化,capacity为16,要么根据传入的值进行初始化;
- Map m = new HashMap(); 创建一个hashmap时,没有进行table数组的初始化,在第一次put操作时,进行table初始化。
(2)put操作后,检查到size已经超过threshold,那么便会执行resize,进行扩容,如果此时capcity已经大于了最大值,那么便把threshold置为int最大值,否则capcity,threshold进行扩容操作。
- 发生了扩容操作,那么必须Map中的所有的数进行再散列,重新装入。
- 扩容很耗性能。所以在使用HashMap的时候,先估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
- resize方法非常巧妙,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。
具体扩容图如下:将一个原先capcity为16的扩容成32的:
在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变(因为任何数与0与都依旧是0),是1的话index变成“原索引+oldCap”。
例如:n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
可以将resize扩容的步骤总结为
1、计算扩容后的容量,临界值。
2、将hashMap的临界值修改为扩容后的临界值
3、根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
4、将旧数组的元素复制到table中。
/** 对table进行初始化或者扩容。
* 如果table为null,则对table进行初始化
* 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。
*/
final Node<K,V>[] resize() {
//新建oldTab数组保存扩容前的数组table
Node<K,V>[] oldTab = table;
//使用变量oldCap扩容前table的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//保存扩容前的临界值
int oldThr = threshold;
int newCap, newThr = 0;
//如果扩容前的容量 > 0
if (oldCap > 0) {
//如果当前容量>=MAXIMUM_CAPACITY
if (oldCap >= MAXIMUM_CAPACITY) {
//扩容临界值提高到正无穷
threshold = Integer.MAX_VALUE;
//无法进行扩容,返回原来的数组
return oldTab;
}
//如果现在容量的两倍小于MAXIMUM_CAPACITY且现在的容量大于DEFAULT_INITIAL_CAPACITY
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)
//临界值变为原来的2倍
newThr = oldThr << 1;
}//如果旧容量 <= 0,而且旧临界值 > 0
else if (oldThr > 0)
//数组的新容量设置为老数组扩容的临界值
newCap = oldThr;
else {//如果旧容量 <= 0,且旧临界值 <= 0,新容量扩充为默认初始化容量,新临界值为DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//在当上面的条件判断中,只有oldThr > 0成立时,newThr == 0
//ft为临时临界值,下面会确定这个临界值是否合法,如果合法,那就是真正的临界值
float ft = (float)newCap * loadFactor;
//当新容量< MAXIMUM_CAPACITY且ft < (float)MAXIMUM_CAPACITY,新的临界值为ft,否则为Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将扩容后hashMap的临界值设置为newThr
threshold = newThr;
//创建新的table,初始化容量为newCap
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//修改hashMap的table为新建的newTab
table = newTab;
//如果旧table不为空,将旧table中的元素复制到新的table中
if (oldTab != null) {
//遍历旧哈希表的每个桶,将旧哈希表中的桶复制到新的哈希表中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果旧桶不为null,使用e记录旧桶
if ((e = oldTab[j]) != null) {
//将旧桶置为null
oldTab[j] = null;
//如果旧桶中只有一个node
if (e.next == null)
//将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置
newTab[e.hash & (newCap - 1)] = e;
//如果旧桶中的结构为红黑树
else if (e instanceof TreeNode)
//将树中的node分离
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { //如果旧桶中的结构为链表。这段没有仔细研究
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;
}
}
}
}
}
return newTab;
}
2.7 remove方法分析
/**
* 删除hashMap中key映射的node
*
* @param key 参数key
* @return 如果没有映射到node,返回null,否则返回对应的value。
*/
public V remove(Object key) {
Node<K,V> e;
//根据key来删除node。removeNode方法的具体实现在下面
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
从源码中可以看到,remove方法的实现可以分为三个步骤:
1、通过hash(Object key)方法计算key的哈希值。
2、通过removeNode方法实现功能。
3、返回被删除的node的value。
下面看看removeNode方法的具体实现,可以将removeNode方法的步骤总结为:
(1)如果数组table为空或key映射到的桶为空,返回null。
(2)如果key映射到的桶上第一个node的就是要删除的node,记录下来。
(3)如果桶内不止一个node,且桶内的结构为红黑树,记录key映射到的node。
(4)桶内的结构不为红黑树,那么桶内的结构就肯定为链表,遍历链表,找到key映射到的node,记录下来。
(5)如果被记录下来的node不为null,删除node,size-1被删除。
(6)返回被删除的node。
代码实现如下:
/** Map.remove和相关方法的实现需要的方法 -- 删除node
* @param hash key的哈希值
* @param key 参数key
* @param value 如果matchValue为true,则value也作为确定被删除的node的条件之一,否则忽略
* @param matchValue 如果为true,则value也作为确定被删除的node的条件之一
* @param movable 如果为false,删除node时不会删除其他node
* @return 返回被删除的node,如果没有node被删除,则返回null(针对红黑树的删除方法)
*/
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;
//如果数组table不为空且key映射到的桶不为空
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;
//如果桶上第一个node的就是要删除的node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//记录桶上第一个node
node = p;
else if ((e = p.next) != null) {//如果桶内不止一个node
if (p instanceof TreeNode)//如果桶内的结构为红黑树
//记录key映射到的node
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {//如果桶内的结构为链表
do {//遍历链表,找到key映射到的node
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
//记录key映射到的node
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//如果得到的node不为null且(matchValue为false||node.value和参数value匹配)
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
//如果桶内的结构为红黑树
if (node instanceof TreeNode)
//使用红黑树的删除方法删除node
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)//如果桶的第一个node的就是要删除的node
//删除node
tab[index] = node.next;
else//如果桶内的结构为链表,使用链表删除元素的方式删除node
p.next = node.next;
//结构性修改次数+1
++modCount;
//哈希表大小-1
--size;
afterNodeRemoval(node);
//返回被删除的node
return node;
}
}
//如果数组table为空或key映射到的桶为空,返回null。
return null;
原文参考链接:https://blog.csdn.net/zjxxyz123/article/details/81111627.
原文参考链接:https://blog.csdn.net/panweiwei1994/article/details/77244920.