HashMap实现原理以及源码解析jdk1.8--put与扩容
1、put源码:
先执行put方法
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or <tt>null</tt> if there was no
* mapping for <tt>key</tt>. (A <tt>null</tt> return can also indicate that the map previously
* associated <tt>null</tt> with <tt>key</tt>.)
*/
/**
* 将指定参数key和指定参数value插入map中,如果key已经存在,那就替换key对应的value
* put(K key, V value)可以分为三个步骤:
* 1.通过hash(Object key)方法计算key的哈希值。
* 2.通过putVal(hash(key), key, value, false, true)方法实现功能。
* 3.返回putVal方法返回的结果。
*
* @param key 指定key
* @param value 指定value
* @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2、put方法中对key进行了hash处理,计算其对应的hash值
/**
* HashMap中键值对的存储形式为链表节点,hashCode相同的节点(位于同一个桶)用链表组织
* hash方法分为三步:
* 1.取key的hashCode
* 2.key的hashCode高16位异或低16位
* 3.将第一步和第二步得到的结果进行取模运算。
*/
static final int hash(Object key) {
int h;
//计算key的hashCode, h = Objects.hashCode(key)
//h >>> 16表示对h无符号右移16位,高位补0,然后h与h >>> 16按位异或
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
整个过程本质上就是三步:
- 拿到 key 的 hashCode 值
- 将 hashCode 的高位参与运算,重新计算 hash 值
- 将计算出来的 hash 值与 (table.length - 1) 进行 & 运算
注:
如果传入的int类型的值:
向一个Object类型赋值一个int的值时,会将int值自动封箱为Integer。
Integer类型的hashcode都是他自身的值,即h=key;
h >>> 16为无符号右移16位,低位挤走,高位补0;
^ 为按位异或,即转成二进制后,相异为1,相同为0.
由此可发现,当传入的值小于 2的16次方-1 时,调用这个方法返回的值,都是自身的值。
4、putVal过程
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
/**
* Map.put和其他相关方法的实现需要的方法
* putVal方法可以分为下面的几个步骤:
* 1.如果哈希表为空,调用resize()创建一个哈希表。
* 2.如果指定参数hash在表中没有对应的桶,即为没有碰撞,直接将键值对插入到哈希表中即可。
* 3.如果有碰撞,遍历桶,找到key映射的节点
* 3.1桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
* 3.2如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方法插入键值对。
* 3.3如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。
* 如果没有找到,在链表尾部插入节点。
* 插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链表转为红黑树。
* 4.如果找到了key映射的节点,且节点不为null
* 4.1记录节点的vlaue。
* 4.2如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。
* 4.3返回记录下来的节点的value。
* 5.如果没有找到key映射的节点(2、3步中讲了,这种情况会插入到hashMap中),插入节点后size会加1,
* 这时要检查size是否大于临界值threshold,如果大于会使用resize方法进行扩容。
*
* @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) {
/* 这里调用resize,其实就是第一次put时,对数组进行初始化。*/
n = (tab = resize()).length;
}
/**
* 如果指定参数hash在表中没有对应的桶,即为没有碰撞.
* (n - 1) & hash 计算key将被放置的槽位.
* (n - 1) & hash 本质上是hash % n,位运算更快.
*/
//如果选定的数组坐标处没有元素,直接放入
if ((p = tab[i = (n - 1) & hash]) == null) {
//位置为空,将i位置上赋值一个node对象
tab[i] = newNode(hash, key, value, null);
} else { // 桶中已经存在元素, 则进入else
Node<K, V> e;
K k;
//如果链表第一个元素或树的根的key与要插入的数元素key 和 hash值完全相同,覆盖旧值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) {
// 将第一个元素赋值给e,用e来记录
e = p;
// 当前桶中无该键值对, 进入else. 此时如果桶是红黑树结构,按照红黑树结构插入,调用putTreeVal
} else if (p instanceof TreeNode) {
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
//p与新节点既不完全相同,p也不是treenode的实例, 即桶是链表结构,按照链表结构插入到尾部
//此时只是hash冲突,并且是链表
} else {
//遍历链表,找到合适的处理方式 1插入新节点 2覆盖旧值
for (int binCount = 0; ; ++binCount) { //一个死循环
//在链表尾部插入新节点
if ((e = p.next) == null) { //e=p.next,如果p的next指向为null
// 先将新节点插入到 p.next
p.next = newNode(hash, key, value, null);
//如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,
// treeifyBin首先判断当前hashMap的长度,如果不足64,只进行resize,扩容table,
// 如果达到64,那么将冲突的存储结构为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
{
//将链表转化为二叉树
treeifyBin(tab, hash);
}
break;
}
// 如果链表中有一个节点key和新插入的key重复,则跳出循环。
// 链表节点的<key, value>与put操作<key, value>相同时,不做重复操作,跳出循环
// 直白一点就是说,如果遍历过程中链表中的元素与新添加的元素完全相同,则跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
//将p中的next赋值给p,即将链表中的下一个node赋值给p, 继续循环遍历链表中的元素
p = e;
}
}
//这个判断中代码作用为:如果添加的元素产生了hash冲突,那么调用put方法时,会将他在链表中他的上一个元素的值返回
if (e != null) { // existing mapping for key
// 记录e的value
V oldValue = e.value;
//判断条件成立的话,将oldvalue替换
if (!onlyIfAbsent || oldValue == null) {
//为newvalue,返回oldvalue;不成立则不替换,然后返回oldvalue
e.value = value;
}
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
//记录修改次数
++modCount;
// 如果元素数量大于临界值,则进行 rehash 扩容
// 扩张也是同步判断和执行的,所以恰好触发时会比较慢
if (++size > threshold) {
resize();
}
// 插入后回调
afterNodeInsertion(evict);
return null;
}
5、putVal执行过程 -- 新元素的插入
-
1、计算元素的位置。hash&(n-1)就是新元素的位置,n代表的是table长度。hash(key) 得到元素位置。
-
2、查看对应位置是否有元素。
-
3、如果没有,直接插入;如果有,进行hash碰撞的处理。
-
4、hash碰撞三种情况
-
一是碰撞的元素与要插入的元素hash值和key都相等,直接进行value的更新;
-
二是结点类型是树结点直接,进行树结点的增加或更新;
-
三是普通结点,只是hash碰撞,key不相同,于是增长链表。
-
5、插入之后,会进行hashmap大小的判断,如果元素数量超过了threshold,则会进行resize()扩容。
每当插入一个元素的时候,就会对这个元素的键的Hash值按此时的数组长度取模,然后装入对应的位置。比如一个hash值为14的元素插入一个table长度为16(源码中为DEFAULT_INITIAL_CAPACITY默认值
)的hashMap中,14对16取模是14(源码中为(n - 1) & hash, 即 (16 - 1) & 14=14
),于是就装入14这个位置。不同的元素取模之后发生碰撞,比如30对16取模也等于14,这样也需要放入14的位置,于是就会在这个桶中形成链表。
6、桶由 链表--》红黑树(树化),红黑树--》链表(树链化)
- 随着数据的不断增加,数组(碰撞导致)和链表(数据新增)长度一直增加,当链表长度超过8,且数组长度不超过64时,链表会被扩容。
- 树化的条件是:桶中链表的长度达到了8,并且数组的长度大于等于64,链表就会被树化成为红黑树结构。
- 在极端情况下: 当连续存储的元素的 hash 相同, 个数达到 11时, 也就是说 table 中只有一个元素, 但是链表长度达到 11, 此时链表也会转树形。
- 随着链表的变短,存储结构又会从红黑树转变为链表。
- 为什么要转换? 因为Map中桶的元素初始化是链表保存的,其查找性能是O(n),而树结构能将查找性能提升到O(log(n))。
- TreeNodes(红黑树)占用空间是普通Nodes(链表)的两倍,为了时间和空间的权衡。
- 链表变成红黑树也只是当前桶挂载的bin会进行转换,不会影响其它桶的数据结构。
- 转为红黑树节点后,链表的结构还存在,通过 next 属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转为红黑树节点,链表结构就不存在了。
- 在红黑树上,叶子节点也可能有 next 节点,因为红黑树的结构跟链表的结构是互不影响的,不会因为是叶子节点就说该节点已经没有 next 节点。
红黑树
`
单个桶链表长度达到8时,其他桶都是为空,桶的结构还是为链表,数组长度为16。
单个桶链表长度达到9时,其他桶还都是为空,桶的结构还是为链表,数组长度为32,扩容了。
单个桶链表长度达到10时,其他桶都是为空,桶的结构还是为链表,数组长度为64。
单个桶链表长度达到11时,其他桶都是为空,桶的结构由链表转变为红黑树(树化),数组长度为64。
单个桶链表长度达到15时,其他桶都是为空,桶的结构是红黑树,插入新的key,插入到树结构中,数组长度为64。
开始移除节点
当移除节点,桶的大小为10,结构还是红黑树。
继续移除节点,桶的大小为6,结构还是红黑树。
当移除节点,桶的大小为5,结构由红黑树转变为链表(红黑树--》链表(树链化))。
插入新的key,放到其他的桶中,桶与桶之间结构互不影响,一个桶是红黑树,另外一个桶可以时链表。
/**
* 将链表转化为红黑树
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index;
Node<K,V> e;
// 如果桶数组table为空,或者桶数组table的长度小于MIN_TREEIFY_CAPACITY,不符合转化为红黑树的条件
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//扩容
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 如果符合转化为红黑树的条件,而且hash对应的桶不为null, 则重新计算 hash段位,及table的索引位,第一个节点
/************ 双向链表 start***************/
// 红黑树的hd头节点, tl尾节点
TreeNode<K,V> hd = null, tl = null;
// 遍历链表
do {
//替换链表node为树node,建立双向链表
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);
/************ 双向链表 end***************/
// 前面仅仅转换为双向链表,treeify才是转换红黑树的处理方法入口
// 第一个节点赋值为头节点,也就是根节点
if ((tab[index] = hd) != null) {
// 将二叉树转换为红黑树
hd.treeify(tab);
}
}
}
HashMap本来是以空间换时间,所以填充比(static final float DEFAULT_LOAD_FACTOR = 0.75f;
)没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。
流程图总结:
![hashmap-put流程图.png](https://i-blog.csdnimg.cn/blog_migrate/b0c49b48f9b37984b7a0624acfef88df.png)
7、HashMap扩容机制
HashMap扩容可以分为三种情况:
-
第一种:使用默认构造方法初始化HashMap(
HashMap()
)。从前文可以知道HashMap在一开始初始化的时候会返回一个空的table,并且thershold未赋值所以为0。
此时oldCap=0, oldThr=thershold=0, 因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。
源代码部分
} else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
// 默认 16 * 0.75 = 12
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
-
第二种:指定初始容量的构造方法初始化HashMap(
HashMap(int initialCapacity)
以及HashMap(int initialCapacity, float loadFactor)
)。那么从下面源码可以看到初始容量会等于threshold,接着 threshold = 当前的容量(threshold)*DEFAULT_LOAD_FACTOR。此时oldCap=0, oldThr=thershold>0。
源代码执行部分else if (oldThr > 0) // initial capacity was placed in threshold { newCap = oldThr; } else {
-
第三种:HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。此时入参为Map测构造方法也可以出发扩容动作(
HashMap(Map<? extends K, ? extends V> m)
).
源代码执行部分if (oldCap > 0) { ...... }
HashMap是先插入数据再进行扩容的,但是如果是刚刚初始化容器的时候是先扩容再插入数据。
构造HashMap表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比0.75*Node.length)时,进行扩容操作,重新调整HashMap大小变为原来2倍大小。扩容很耗时。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
/**
* 对table进行初始化或者扩容。
* 如果table为null,则对table进行初始化
* 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,
* 节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。
*
* resize的步骤总结为:
* 1.计算扩容后的容量,临界值。
* 2.将hashMap的临界值修改为扩容后的临界值
* 3.根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
* 4.将旧数组的元素复制到table中。
*
* @return the table
*/
final Node<K, V>[] resize() {
//新建oldTab数组保存扩容前的数组table
Node<K, V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//默认构造器的情况下为0
int oldThr = threshold;
int newCap, newThr = 0;
//如果旧表的长度不是空, 扩容肯定执行这个分支
if (oldCap > 0) {
//当前table容量大于最大值得时候返回当前table. 此时loadfactor很大,冲突量比较大,查询不再是O(1)
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
// 把新表的长度设置为旧表长度的两倍,newCap=2*oldCap
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) {
//扩容容量为2倍,临界值为2倍
newThr = oldThr << 1; // double threshold
}
// 如果旧表的长度的是0,就是说第一次初始化表
} else if (oldThr > 0) // initial capacity was placed in threshold
{
//使用带有初始容量的构造器时,table容量为初始化得到的threshold
newCap = oldThr;
} else { // zero initial threshold signifies using defaults
//默认构造器下进行扩容
newCap = DEFAULT_INITIAL_CAPACITY;
// 默认 16 * 0.75 = 12
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"})
// 开始构造新表, 按newCap创建新的数组,初始化表中的数据
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
//新的数组赋值给table
table = newTab;
//扩容后,对新扩容后的table赋值,重新计算元素新的位置。 原表不是空要把原表中数据移动到新表中
if (oldTab != null) { // oldCap 原数组
//遍历原来的旧表
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
//判断当前遍历下的该node是否为空,将j位置上的节点保存到e, 然后将oldTab[j]置为空。
if ((e = oldTab[j]) != null) {
// 为什么要置为空,有什么好处?? TODO
oldTab[j] = null;
///普通节点, 位置是hash求余。 如果为null 说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置
if (e.next == null) {
newTab[e.hash & (newCap - 1)] = e;
} else if (e instanceof TreeNode) {
// 树形结构修剪. 当扩容时,
// 如果当前桶中元素结构是红黑树,并且元素个数小于链表还原阈值 UNTREEIFY_THRESHOLD (默认为 6),就会把桶中的树形结构缩小或者直接还原(切分)为链表结构
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
// 如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重新计算在新表的位置,并进行搬运
} else { // preserve order 保证顺序
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
/*
这里如果判断成立,那么该元素的地址在新的数组中就不会改变。
因为oldCap的最高位的1,在e.hash对应的位上为0,所以扩容后得到的地址是一样的,位置不会改变 ,在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j];
如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,也就是lodCap最高位的1,在e.hash对应的位置上也为1,所以扩容后的地址改变了,在后面的代码中会放到hiHead中,最后赋值给newTab[j + oldCap]
举个例子来说一下上面的两种情况:
设:oldCap=16 二进制为:0001 0000
oldCap-1=15 二进制为:0000 1111
e1.hash=10 二进制为:0000 1010
e2.hash=26 二进制为:0101 1010
e1在扩容前的位置为:e1.hash & oldCap-1 结果为:0000 1010
e2在扩容前的位置为:e2.hash & oldCap-1 结果为:0000 1010
结果相同,所以e1和e2在扩容前在同一个链表上,这是扩容之前的状态。
现在扩容后,需要重新计算元素的位置,在扩容前的链表中计算地址的方式为e.hash & oldCap-1
那么在扩容后应该也这么计算,扩容后的容量为oldCap*2=32,2^5, 二进制为:0010 0000。 所以 newCap=32,
新的计算方式应该为
e1.hash & newCap-1
即:0000 1010 & 0001 1111
结果为0000 1010与扩容前的位置完全一样。
e2.hash & newCap-1 即:0101 1010 & 0001 1111
结果为0001 1010,为扩容前位置+oldCap。
而这里却没有e.hash & newCap-1 而是 e.hash & oldCap,其实这两个是等效的,都是判断倒数第五位是0,还是1。
如果是0,则位置不变,是1则位置改变为扩容前位置+oldCap。
再来分析下loTail, loHead这两个的执行过程(假设(e.hash & oldCap) == 0成立):
第一次执行:
e指向oldTab[j]所指向的node对象,即e指向该位置上链表的第一个元素.
loTail为空,所以loHead指向与e相同的node对象(loHead = e;),然后loTail也指向了同一个node对象(loTail = e;)。
最后,在判断条件e指向next,就是指向oldTab链表中的第二个元素
第二次执行:
lotail不为null,所以lotail.next指向e,这里其实是lotail指向的node对象的next指向e,
也可以说是,loHead的next指向了e,就是指向了oldTab链表中第二个元素。此时loHead指向
的node变成了一个长度为2的链表。然后lotail=e也就是指向了链表中第二个元素的地址。
第三次执行:
与第二次执行类似,loHead上的链表长度变为3,又增加了一个node,loTail指向新增的node......
hiTail与hiHead的执行过程与以上相同。
由此可以看出,loHead是用来保存新链表上的头元素的,loTail是用来保存尾元素的,直到遍历完链表。
这是(e.hash & oldCap) == 0成立的时候。
(e.hash & oldCap) == 0不成立的情况也相同,其实就是把oldCap遍历成两个新的链表,
通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与newTab[j+oldCap]上面去。
*/
do {
//记录下一个结点
next = e.next;
// 新表是旧表的两倍容量,实例上就把单链表拆分为两队, e.hash&oldCap==0为偶数一队,反之为奇数一对
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);
//lo队不为null,放在新表原位置
if (loTail != null) {
//尾节点的next设置为空
loTail.next = null;
newTab[j] = loHead;
}
//hi队不为null,放在新表j+oldCap位置
if (hiTail != null) {
//尾节点的next设置为空
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
8、链表中移除一个节点只需如下图操作,其他操作同理。
9、红黑树在维护链表结构时,移除一个节点只需如下图操作(红黑树中增加了一个 prev 属性),其他操作同理。注:此处只是红黑树维护链表结构的操作,红黑树还需要单独进行红黑树的移除或者其他操作。
10、源码中进行红黑树的查找时,会反复用到以下两条规则:1)如果目标节点的 hash 值小于 p 节点的 hash 值,则向 p 节点的左边遍历;否则向 p 节点的右边遍历。2)如果目标节点的 key 值小于 p 节点的 key 值,则向 p 节点的左边遍历;否则向 p 节点的右边遍历。这两条规则是利用了红黑树的特性(左节点 < 根节点 < 右节点)。
11、remove 方法
/**
* 移除某个节点
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
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.如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p
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;
// 2.如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 3.否则将p.next赋值给e,向下遍历节点
// 3.1 如果p是TreeNode则调用红黑树的方法查找节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 3.2 否则,进行普通链表节点的查找
do {
// 当节点的hash值和key与传入的相同,则该节点即为目标节点
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; // 赋值给node, 并跳出循环
break;
}
p = e; // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点
} while ((e = e.next) != null); // e指向下一个节点
}
}
// 4.如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 4.1 如果是TreeNode则调用红黑树的移除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 4.2 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点,
// “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点
else if (node == p)
tab[index] = node.next;
// 4.3 否则将node的上一个节点的next属性设置为node的next节点,
// 即将node节点移除, 将node的上下节点进行关联(链表的移除)
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node); // 供LinkedHashMap使用
// 5.返回被移除的节点
return node;
}
}
return null;
}
removeTreeNode:移除调用此方法的节点,也就是该方法中的 this 节点。移除包括链表的处理和红黑树的处理。
/**
* 红黑树的节点移除
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
// --- 链表的处理start ---
int n;
// 1.table为空或者length为0直接返回
if (tab == null || (n = tab.length) == 0)
return;
// 2.根据hash计算出索引的位置
int index = (n - 1) & hash;
// 3.将索引位置的头节点赋值给first和root
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
// 4.该方法被将要被移除的node(TreeNode)调用, 因此此方法的this为要被移除node节点,
// 将node的next节点赋值给succ节点,prev节点赋值给pred节点
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
// 5.如果pred节点为空,则代表要被移除的node节点为头节点,
// 则将table索引位置的值和first节点的值赋值为succ节点(node的next节点)即可
if (pred == null)
tab[index] = first = succ;
else
// 6.否则将pred节点的next属性设置为succ节点(node的next节点)
pred.next = succ;
// 7.如果succ节点不为空,则将succ的prev节点设置为pred, 与前面对应
if (succ != null)
succ.prev = pred;
// 8.如果进行到此first节点为空,则代表该索引位置已经没有节点则直接返回
if (first == null)
return;
// 9.如果root的父节点不为空, 则将root赋值为根节点
if (root.parent != null)
root = root.root();
// 10.通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回
// (转链表后就无需再进行下面的红黑树处理)
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
// --- 链表的处理end ---
// --- 以下代码为红黑树的处理 ---
// 11.将p赋值为要被移除的node节点,pl赋值为p的左节点,pr赋值为p 的右节点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
// 12.如果p的左节点和右节点都不为空时
if (pl != null && pr != null) {
// 12.1 将s节点赋值为p的右节点
TreeNode<K,V> s = pr, sl;
// 12.2 向左一直查找,跳出循环时,s为没有左节点的节点
while ((sl = s.left) != null)
s = sl;
// 12.3 交换p节点和s节点的颜色
boolean c = s.red; s.red = p.red; p.red = c;
TreeNode<K,V> sr = s.right; // s的右节点
TreeNode<K,V> pp = p.parent; // p的父节点
// --- 第一次调整和第二次调整:将p节点和s节点进行了位置调换 ---
// 12.4 第一次调整
// 如果p节点的右节点即为s节点,则将p的父节点赋值为s,将s的右节点赋值为p
if (s == pr) {
p.parent = s;
s.right = p;
}
else {
// 将sp赋值为s的父节点
TreeNode<K,V> sp = s.parent;
// 将p的父节点赋值为sp
if ((p.parent = sp) != null) {
// 如果s节点为sp的左节点,则将sp的左节点赋值为p节点
if (s == sp.left)
sp.left = p;
// 否则s节点为sp的右节点,则将sp的右节点赋值为p节点
else
sp.right = p;
}
// s的右节点赋值为p节点的右节点
if ((s.right = pr) != null)
// 如果pr不为空,则将pr的父节点赋值为s
pr.parent = s;
}
// 12.5 第二次调整
// 将p的左节点赋值为空,pl已经保存了该节点
p.left = null;
// 将p节点的右节点赋值为sr,如果sr不为空,则将sr的父节点赋值为p节点
if ((p.right = sr) != null)
sr.parent = p;
// 将s节点的左节点赋值为pl,如果pl不为空,则将pl的父节点赋值为s节点
if ((s.left = pl) != null)
pl.parent = s;
// 将s的父节点赋值为p的父节点pp
// 如果pp为空,则p节点为root节点, 交换后s成为新的root节点
if ((s.parent = pp) == null)
root = s;
// 如果p不为root节点, 并且p是pp的左节点,则将pp的左节点赋值为s节点
else if (p == pp.left)
pp.left = s;
// 如果p不为root节点, 并且p是pp的右节点,则将pp的右节点赋值为s节点
else
pp.right = s;
// 12.6 寻找replacement节点,用来替换掉p节点
// 12.6.1 如果sr不为空,则replacement节点为sr,因为s没有左节点,所以使用s的右节点来替换p的位置
if (sr != null)
replacement = sr;
// 12.6.1 如果sr为空,则s为叶子节点,replacement为p本身,只需要将p节点直接去除即可
else
replacement = p;
}
// 13.承接12点的判断,如果p的左节点不为空,右节点为空,replacement节点为p的左节点
else if (pl != null)
replacement = pl;
// 14.如果p的右节点不为空,左节点为空,replacement节点为p的右节点
else if (pr != null)
replacement = pr;
// 15.如果p的左右节点都为空, 即p为叶子节点, replacement节点为p节点本身
else
replacement = p;
// 16.第三次调整:使用replacement节点替换掉p节点的位置,将p节点移除
if (replacement != p) { // 如果p节点不是叶子节点
// 16.1 将p节点的父节点赋值给replacement节点的父节点, 同时赋值给pp节点
TreeNode<K,V> pp = replacement.parent = p.parent;
// 16.2 如果p没有父节点, 即p为root节点,则将root节点赋值为replacement节点即可
if (pp == null)
root = replacement;
// 16.3 如果p不是root节点, 并且p为pp的左节点,则将pp的左节点赋值为替换节点replacement
else if (p == pp.left)
pp.left = replacement;
// 16.4 如果p不是root节点, 并且p为pp的右节点,则将pp的右节点赋值为替换节点replacement
else
pp.right = replacement;
// 16.5 p节点的位置已经被完整的替换为replacement, 将p节点清空, 以便垃圾收集器回收
p.left = p.right = p.parent = null;
}
// 17.如果p节点不为红色则进行红黑树删除平衡调整
// (如果删除的节点是红色则不会破坏红黑树的平衡无需调整)
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
// 18.如果p节点为叶子节点, 则简单的将p节点去除即可
if (replacement == p) {
TreeNode<K,V> pp = p.parent;
// 18.1 将p的parent属性设置为空
p.parent = null;
if (pp != null) {
// 18.2 如果p节点为父节点的左节点,则将父节点的左节点赋值为空
if (p == pp.left)
pp.left = null;
// 18.3 如果p节点为父节点的右节点, 则将父节点的右节点赋值为空
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
// 19.将root节点移到索引位置的头节点
moveRootToFront(tab, r);
}
图解:removeTreeNode 图解
下面的图解是代码中的最复杂的情况,即流程最长的那个,p 节点不为根节点,p 节点有左右节点,s 节点不为 pr 节点,s 节点有右节点。
另外,第一次调整和第二次调整的是本人根据代码而设定的,将第一次调整和第二次调整合起来看会更容易理解,如下:
- 第一次调整 + 第二次调整:将 p 节点和 s 节点进行了位置调换,选出要替换掉 p 节点的 replacement
- 第三次调整:将 replacement 节点覆盖掉 p 节点
参考资料:
http://blog.csdn.net/v123411739/article/details/78996181