目录

概述

原理简述

新旧版本对比

结构设计原理

继承关系

成员变量

核心:为什么负载因子设定为0.75?

核心:为什么树化的链表阈值是8?

核心:为什么树退化的链表阈值是6?

构造函数

默认构造方法

传入初始容量大小的构造方法

传入初始容量及负载因子的构造方法

核心:传入初始容量位运算的原理

传入Map对象的构造方法

数据结构

链表(开环单向)

核心:死循环场景复原

红黑树

红黑树特性

核心:如何做到最长路径不超过最短路径的两倍?

核心:插入情况及旋转触发条件

重点方法分析

put方法

核心:存储时键的哈希运算原理

putTreeVal方法

treeifyBin方法

balanceInsertion方法

rotateLeft方法

核心:左旋情况一演示

核心:左旋情况二演示

rotateRight方法

核心:右旋情况一演示

核心:右旋情况二演示

resize方法

核心:扩容时旧链表的节点计算新的索引位的计算原理

remove方法

核心:为什么当目标节点是红黑树时,置换节点的子右是替换目标节点的首选

核心:红黑树移除节点演示

get方法


概述

HashMap是常用的键值对存储结构,一个键对应一个值,并且键允许为null,值也允许为null,但由于其哈希性质,键不可以重复,因此只有一个键可为nullHashMap无法保证有序,因此放入顺序与取出顺序无法一致,且线程不安全。

原理简述

已知JDK1.8版本的Hashmap由桶,链表、红黑树共同组成,每个桶位存储的都是链表的头部节点,而链表存储的都是键的哈希值存在冲突的数据,冲突的发生导致了链表结构的生成,冲突越多,链表结构越长,考虑到查询效率的问题,当链表超过一定长度时会由链表转换成红黑树来降低查询的时间复杂度,同时若冲突随着数据的移除而减少后,长度小于一个阈值,则会由红黑树转换回链表结构来降低结构的复杂度和树的平衡成本。

新旧版本对比

版本

JDK1.7

JDK1.8

结构

桶,链表

桶,链表,红黑树

性能

O(n)

O(logn)

结构设计原理

继承关系

关系

能力

对象

implements

接口方法

Map

implements

克隆模式

Cloneable

implements

序列化

Serializable

extend

抽象方法

AbstractMap

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializabl

成员变量

// 初始化Node数组(桶)的长度,必须是2的幂次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
​
// Node最大容量不能大于1<<30(即2的30次幂)
static final int MAXIMUM_CAPACITY = 1 << 30;
​
// 构造函数未指定负载因子时,默认的加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
​
// 当一条链表长度达到8时,链表会转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
​
// 当红黑树元素小于6个时,转换为链表结构
static final int UNTREEIFY_THRESHOLD = 6;
​
// 链表转变成树之前,还会有一次判断,只有Node数组(桶)长度大于 64 才会发生转换
static final int MIN_TREEIFY_CAPACITY = 64;
​
// Node数组(桶)
transient Node<K,V>[] table;
​
// Entry的Set集合
transient Set<Map.Entry<K,V>> entrySet;
​
// Hashmap中存储实例的个数
transient int size;
​
// 凡是我们做的增删改都会引发modCount值的变化,跟版本控制功能类似,
// 可以理解成version,在特定的操作下需要对version进行检查,适用于Fail-Fast机制
transient int modCount;
​
// 扩容阈值 计算方式为: capacity * loadFactor
int threshold;
​
// 可自定义的负载因子,不过一般都是用系统自带的0.75
final float loadFactor;

核心:为什么负载因子设定为0.75?

负载因子loadFactor用作扩容时容量阈值的调和参数,计算方式:

allNodes > (capacity * loadFacotr) ,即存储的节点总数大于某个数量级就会触发扩容,而负载因子设定在0.75是为了考虑结构在时间和空间的成本折中的一个平衡数值,负载因子设定较高,扩容放缓,红黑树长时间处于树枝较深的状态导致查询效率降低,但也会降低内存空间的开销。而负载因子设定较低,扩容频繁,降低了树高度提高了查询效率,但相应的内存开销也会变大。

核心:为什么树化的链表阈值是8?

主要来看官方给出的解释:红黑树占用的空间大小大约是链表的两倍,因此并非一开始就使用红黑树来解决哈希冲突的问题,即便冲突过载结构进化为红黑树,也会随着表的删除等操作致使冲突减少而将红黑树退化为链表。当键的哈希离散性较好时,红黑树使用的几率不大。即使在随机离散的情况下,容器中节点的分布频率遵循泊松分布,并且由于负载均衡因子的设定,超过一定阈值会扩容重新作哈希分布计算,每个链表中哈希冲突的个数大于8的概率为0.00000006,对撞概率极低,因此链表的阈值设定为8是概率统计的结果。

以下为官方释义:

* Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million

核心:为什么树退化的链表阈值是6?

当红黑树结构生成后,存在因冲突减少而使结构退化为链表的可能,已知结构跃迁的阈值是一个冲突链上有8个节点,那么8就是临界点,考虑冲突的数量可能在一段时间内徘徊于临界点,导致结构在链表和红黑树之间不断地转换,造成不必要的开销,因此退化的阈值要稍小于树化的阈值。

构造函数

默认构造方法

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;
}

传入初始容量大小的构造方法

public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

传入初始容量及负载因子的构造方法

核心:传入初始容量位运算的原理

已知 Hashmap 规定桶的长度必须是,当使用自定义初始容量时,为了满足规定大小需要通过计算来调整。例如容量自定义为10或15时,容器会调整为16,自定义为17或29时,容器调整为32。这种调整都是通过tableSizeFor 的位运算完成的。

源码如下所示,共有5次右移运算,5次或运算,目的是让含1的比特位移动后的比特位也是1(若不进行或运算,比特位右移,而原位置得高位补充可能是0,则未达到将低位全部填充为1的目的)。

公式 n |= n >>> x 解释为参与计算的比特位的数量是 2x ,致使除了最高位其余位都参与了或计算,得出结果为低位全是1的二进制数,而低位全是1,该数无法满足,所以还需在最后 n+1,得到1个最小的比n大的。而 int n = cap - 1 是为了处理自定义的容量值本身就满足

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);
}
​
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;
}

传入Map对象的构造方法

public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);
}

数据结构

链表(开环单向)

结构组成

static class Node<K,V> implements Map.Entry<K,V> {final int hash; // 数组哈希索引位置final K key;// 键V value;// 值Node<K,V> next;// 下一节点……
}

核心:死循环场景复原

发生场景:并发插入元素,导致扩容后链表闭环。

版本:JDK1.7

代码如下:

public V put(K key, V value){......int hash = hash(key.hashCode());// 计算索引位置int i = indexFor(hash, table.length);for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}modCount++;// 该key不存在,则在桶中新加一个索引位置addEntry(hash, key, value, i);return null;
}void addEntry(int hash, K key, V value, int bucketIndex){Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<K,V>(hash, key, value, e);// 若节点数量超过阈值threshold,则要扩容if (size++ >= threshold)resize(2 * table.length);
} void resize(int newCapacity){Entry[] oldTable = table;int oldCapacity = oldTable.length;......// 创建新桶Entry[] newTable = new Entry[newCapacity];// 旧数据迁移transfer(newTable);table = newTable;threshold = (int)(newCapacity * loadFactor);
}void transfer(Entry[] newTable){Entry[] src = table;int newCapacity = newTable.length;//  从旧的桶中遍历,遍历出来的都是链表的头部节点,依次迁移for (int j = 0; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {// 原桶索引置为null,方便垃圾回收src[j] = null;// 扩容机制将原链表倒装致新桶位中do {// --------线程挂起,并发扩容时出现问题的代码块-------// 拎出下一个节点Entry<K,V> next = e.next;int i = indexFor(e.hash, newCapacity);// --------线程挂起,并发扩容时出现问题的代码块-------// -------倒装-------e.next = newTable[i];               newTable[i] = e;e = next;// -------倒装-------} while (e != null);}}
} public V get(Object key) {if (key == null)return getForNullKey();Entry<K,V> entry = getEntry(key);return null == entry ? null : entry.getValue();
}final Entry<K,V> getEntry(Object key) {// 判断桶内是否有元素if (size == 0) {return null;}int hash = (key == null) ? 0 : hash(key);// 如果并发导致了链表闭环,则这里就会进入死循环for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {Object k;// 判断key是否相同,相同则返回对应的值if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;}return null;
}

红黑树

结构组成

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 父节点TreeNode<K,V> left;// 左子树TreeNode<K,V> right;// 右子树TreeNode<K,V> prev;    // 前节点,需要在删除后解除链接boolean red;// 颜色属性……
}

红黑树特性

保持树平衡的两种方式:变色、旋转(左旋、右旋)

性质1

节点是红色或黑色

性质2

根节点是黑色

性质3

每个叶节点(NIL节点,空节点)是黑色的

性质4

每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点,允许连续的黑色节点)

性质5

从任一节点到其每个叶子的路径上包含的黑色节点数量都相同

性质6

通过键的哈希值来判断,插入时是放入左子节点还是右子节点,节点从左到从哈希值依次递增

上面6条性质约束了以下红黑树的关键特性:

  • 从根到叶子节点的最长路径,不会超过最短路径的两倍。
  • 达到基本平衡,虽然无法绝对平衡,但依然可以保持高效(时间复杂度较链表仍是降低的)。

核心:如何做到最长路径不超过最短路径的两倍?

  • 性质4决定了路径上不能有两个相连的红色节点,最长路径一定是红色点和黑色节点交替而成的。
  • 性质5决定了所有路径上都有相同数目的黑色节点,这就表明了没有路径能多于其他任何路径两倍长。

最短最坏情况为全部黑色,数值取 shortBlack = a

最长最坏情况为黑色节点数大于红色节点数,且红色黑色交替,数值取 longBlack = blongRed = c,由于所有路径黑色数量相同,可以同时满足 a = bc < b 两个条件,因此可以得出结论 (b + c) < 2a

核心:插入情况及旋转触发条件

条件一

插入不能是根节点,否则自平衡

条件二

插入时父节点不可以是黑色,或插入时树的深度不能为2,否则自平衡

条件三

父节点是祖父节点的左子节点(此时不考虑当前节点是子左还是子右)

  • 祖父节点的子右不能为nil且呈红色,右叔节点置黑,父节点置黑,祖父节点置红,进入下一个循环旋转
  • 祖父节点的子右为nil或呈黑色,则开始判断当前节点是子左还是子右
    • 当前节点是子右,则对父节点开始左旋,子左则不左旋
    • 若此时父节点和祖父节点都不为nil,则对祖父节点开始右旋

条件四

父节点是祖父节点的右子节点(此时不考虑当前节点是子左还是子右)

  • 祖父节点的子左不能为nil且呈红色,左叔节点置黑,父节点置黑,祖父节点置红,进入下一个循环旋转
  • 祖父节点的子右为nil或是呈黑色,则开始判断当前节点是子左还是子右
    • 当前节点是子左,则对父节点开始右旋,子右则不右旋
    • 若此时父节点和祖父节点都不为nil,则对祖父节点开始左旋

重点方法分析

put方法

方法逻辑梳理:

  1. 对键作哈希处理,降低冲突。
  2. 桶是空状态,则初始化扩容,懒加载机制。
  3. 计算桶的索引位置是否为空,不为空则发生了冲突,为空则设定插入值为链表头节点。
  4. 发生哈希冲突,判断是否是相同的键,相同则覆盖Value,不同则判断是否是红黑树节点,是则插入红黑树叶子节点上,不是红黑树结构则插入链表尾部并判断是否需要树化。
  5. 判断表内的容量大小是否超过阈值,超过则触发扩容。
public V put(K key, V value) {// 先将key值做哈希运算return putVal(hash(key), key, value, false, true);
}
​
static final int hash(Object key) {int h;// 将key的hashCode和其本身右移16位后的二进制作异或计算return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
​
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 1 检查是否是空桶,空桶则要初始化扩容,懒加载机制if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 2 通过hash计算桶的索引位置,该位置为空则未冲突,直接在该处创建新的链表头节点if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);// 3 索引位置不为空,则哈希冲突,开始解决冲突else {Node<K,V> e; K k;// 4 判断冲突节点p的key和hash值是否跟传入的相等,若相等则未冲突,直接覆盖if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 5 冲突节点的键和要插入的键不同,判断节点是否为红黑树,是则调用红黑树解决冲突else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 6 冲突节点的键和要插入的键不同,使用链表解决冲突,链表长度超过8要树化else {for (int binCount = 0; ; ++binCount) {// 6.1 如果冲突节点的下一节点为空,直接插入到下一个节点(即尾部节点)if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 6.2 是否转为红黑树,-1是因为循环是从冲突节点p的next开始的if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 6.3 如果e节点存在hash值和key值都与传入的相同,则e节点即要插入的键,上次插入时冲突已被处理过,跳出循环if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;// 6.4 变量改为下一节点继续循环p = e;}}// 7 如果e节点不为空,则代表要插入的键存在,使用传入的value覆盖该节点的value,并返回oldValueif (e != null) { // existing mapping for keyV oldValue = e.value;// 7.1 onlyIfAbsent 是公共属性,如果是true, 则不改变已存在的值,或旧值是null则改变该值if (!onlyIfAbsent || oldValue == null)e.value = value;// 用于LinkedHashMapafterNodeAccess(e);return oldValue;}}++modCount;// 8 如果插入节点后节点数超过阈值,则调用resize方法进行扩容if (++size > threshold)resize();// 用于LinkedHashMapafterNodeInsertion(evict);return null;
}

核心:存储时键的哈希运算原理

(h = key.hashCode()) ^ (h >>> 16) 计算将键的哈希值,与哈希值右移了16位后的值,作了异或的运算,目的是为了与 (n - 1) & hash 搭配使用时降低哈希冲突,(n - 1) & hash 是为了计算桶的索引位置,已知n 是桶的长度,而初始化的 Hashmap 的桶默认长度时16,当计算 (16 - 1) & hash 结果只有低位是1,高位全部是0,0的与运算结果必然是0,这种情况下,索引计算结果更多依赖于低位的值,冲突的概率比较高,而键的哈希值作了向右移动16位的运算,让高位也参与到索引位置计算中,可以降低冲突的概率,而桶的长度设置为也是为了保证 (n - 1) 时低位全部是1,高位全部是0,索引 (n - 1) & hash 计算后的结果不会超过桶的长度。

putTreeVal方法

该方法是链表冲突长度超过8,并且已经有一颗红黑树的前提下,直接将值插入到红黑树当中。红黑树插入会同时维护原来的单向链表结构和属性,并非由链表打散后生成红黑树。

方法逻辑梳理:

  1. 寻根,从红黑树根部节点开始操作,找到合适的插入位置。
  2. 根据键的哈希值大小判定插入左支还是右支树节点,传入的键的哈希值小于比较节点的哈希值则从左查找,否则从右查找。
  3. 未实现Comparable接口或比较后相等,或键的class类名来比较大小也相等,则使用类System.identityHashCode 比较哈希大小。
  4. 找到合适插入点,插入红黑树最叶子节点,并且插入到链表结构中。
  5. 此时树还未平衡,要做左旋或右旋以及节点变色保持树平衡。
  6. 此时桶位上的头节点不一定是红黑树的root节点,调整root节点到桶位的头节点上。
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;// 1 查找根节点, 索引位置的头节点并不一定为红黑树的根节点TreeNode<K,V> root = (parent != null) ? root() : this;// 2 将根节点赋值给p节点,开始进行查找for (TreeNode<K,V> p = root;;) {int dir, ph; K pk;// 3 插入的hash值小于p节点的hash值,将dir设为-1,代表向左查找树if ((ph = p.hash) > h)dir = -1;// 4 插入的hash值大于p节点的hash值,将dir赋值为1,代表向右查找树else if (ph < h)dir = 1;// 5 插入的key与其hash值等于p节点的key与hash值,则直接返回p节点else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;// 6 如果键class对象为null 并且 (键对象是否未实现过Comparable 或 键对象实现了Comparable且与p节点的键对象类型相同进行compareTo比较是左查还是右查)else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) {// 6.1 从p节点的左右子节点分别查找遍历,如果查找到插入键则返回if (!searched) {TreeNode<K,V> q, ch;searched = true;// 6.2 p节点下左右查询,查到了则返回已存在的插入键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;}// 6.2 否则使用定义的一套规则来比较k和p节点的key的大小,用来决定向左还是向右查找dir = tieBreakOrder(k, pk);}TreeNode<K,V> xp = p;// 7 dir<=0 则向p左边查找,否则向p右边查找,如果为null,则代表该位置即可插入,否则进入下一个循环查找合适位置if ((p = (dir <= 0) ? p.left : p.right) == null) {// 查找插入节点的父节点的链表结构中的next节点Node<K,V> xpn = xp.next;// 8 创建新的节点, 插入节点放入xp和xpn中间TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);// 9 根据dir是否<=0, 来设定x是xp的子左还是子右if (dir <= 0)xp.left = x;elsexp.right = x;xp.next = x;x.parent = x.prev = xp;// 10 如果xpn不为空,设置x为xpn的前节点if (xpn != null)((TreeNode<K,V>)xpn).prev = x;// 11 balanceInsertion进行树平衡操作// 12 moveRootToFront调整root节点到桶位的头节点上moveRootToFront(tab, balanceInsertion(root, x));return null;}}
}final TreeNode<K,V> root() {// 向上遍历获取根节点for (TreeNode<K,V> r = this, p;;) {if ((p = r.parent) == null)return r;r = p;}
}static Class<?> comparableClassFor(Object x) {// 1 判断x是否实现了Comparable接口if (x instanceof Comparable) {Class<?> c; Type[] ts, as; Type t; ParameterizedType p;// 2 校验x是否为String类型if ((c = x.getClass()) == String.class) // bypass checksreturn c;if ((ts = c.getGenericInterfaces()) != null) {// 3 遍历x实现的所有接口for (int i = 0; i < ts.length; ++i) {// 4 如果x实现了Comparable接口,则返回x的Classif (((t = ts[i]) instanceof ParameterizedType) &&((p = (ParameterizedType)t).getRawType() ==Comparable.class) &&(as = p.getActualTypeArguments()) != null &&as.length == 1 && as[0] == c) // type arg is creturn c;}}}return null;
}static int compareComparables(Class<?> kc, Object k, Object x) {// kc:插入键class对象,k:插入键,x:当前树节点的键// 如果当前树节点的键为null 或 x的class对象与kc类型相同,则返回0,代表不分左右查询// 否则进行compareTo比较,返回左右方向return (x == null || x.getClass() != kc ? 0 :((Comparable)k).compareTo(x));
}final TreeNode<K,V> find(int h, Object k, Class<?> kc) {// h:插入键hash值,k:插入键, kc:插入键class对象// 1 调用此方法的节点赋值给pTreeNode<K,V> p = this;// 2 从p节点开始向下遍历do {int ph, dir; K pk;TreeNode<K,V> pl = p.left, pr = p.right, q;// 3 插入的hash值小于p节点的hash值,向左遍历if ((ph = p.hash) > h)p = pl;// 4 插入的hash值大于p节点的hash值,向右遍历else if (ph < h)p = pr;// 5 插入的key与其hash值等于p节点的key与hash值,直接返回p节点else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;// 6 p节点的子左为空,向右遍历else if (pl == null)p = pr;// 7 p节点的子右为空,向左遍历else if (pr == null)p = pl;// 8 若比较的hash值相等,则根据comparable方式比较大小else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)p = (dir < 0) ? pl : pr;// 9 若key所属类未实现Comparable, 直接指定向右遍历else if ((q = pr.find(h, k, kc)) != null)return q;// 10 向右遍历为空, 改为向左遍历elsep = pl;} while (p != null);return null;
}static int tieBreakOrder(Object a, Object b) {int d;if (a == null || b == null ||(d = a.getClass().getName().compareTo(b.getClass().getName())) == 0)d = (System.identityHashCode(a) <= System.identityHashCode(b) ?-1 : 1);return d;
}// 调整的不是红黑树里面的排列顺序,是维护的违双向链表的排列顺序,调整root到头节点
// 并判断头节点是否有前节点,有得话则移到第二位上
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {int n;// 1 校验root是否为空、table是否为空、table的length是否大于0if (root != null && tab != null && (n = tab.length) > 0) {// 2 计算root节点的索引位置int index = (n - 1) & root.hash;TreeNode<K,V> first = (TreeNode<K,V>)tab[index];// 3 如果该索引位置的头节点不是root节点,则该索引位置的头节点替换为root节点if (root != first) {Node<K,V> rn;// 3.1 将该索引位置的头节点赋值为root节点tab[index] = root;TreeNode<K,V> rp = root.prev;// 3.2 如果root节点的next节点不为空,则将root节点的next节点的prev属性设置为root节点的prev节点if ((rn = root.next) != null)((TreeNode<K,V>)rn).prev = rp;// 3.3 如果root节点的prev节点不为空,则将root节点的prev节点的next属性设置为root节点的next节点if (rp != null)rp.next = rn;// 3.4 如果原头节点不为空, 则将原头节点的prev属性设置为root节点if (first != null)first.prev = root;// 3.5 将root节点的next属性设置为原头节点root.next = first;// 3.6 root此时已经被放到该位置的头节点位置,因此将prev属性设为空root.prev = null;}// 4 检查树是否正常assert checkInvariants(root);}
}static <K,V> boolean checkInvariants(TreeNode<K,V> t) {TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,tb = t.prev, tn = (TreeNode<K,V>)t.next;if (tb != null && tb.next != t)return false;if (tn != null && tn.prev != t)return false;if (tp != null && t != tp.left && t != tp.right)return false;if (tl != null && (tl.parent != t || tl.hash > t.hash))return false;if (tr != null && (tr.parent != t || tr.hash < t.hash))return false;// 如果当前节点为红色, 则该节点的子左子右都不能为红色if (t.red && tl != null && tl.red && tr != null && tr.red)return false;if (tl != null && !checkInvariants(tl))return false;if (tr != null && !checkInvariants(tr))return false;return true;
}

treeifyBin方法

该方法是链表转红黑树方法,触发条件是当链表的插入冲突超出阈值8便树化。转化成红黑树后仍然维护链表关系,但是此时维护的链表成为了开环违双向链表,因为next指针正常,但是prev指针并不是线性的。

方法逻辑梳理:

  1. 判断一次桶是否为空和长度阈值,是否需要进行扩容。
  2. 循环链表上的每个节点,先构建出红黑树节点,并关联好前后节点指针,此时还未生成分支结构。
  3. 红黑树开始生成左右树枝,先确定根节点,后依次将next节点放入子左或子右,且每经历一个next节点处理,都进行一次平衡操作。
final void treeifyBin(Node<K,V>[] tab, int hash) {// tab:桶,hash:插入键hash值int n, index; Node<K,V> e;// 1 判断一次桶是否为空,或长度小于64, 否则进行一次扩容if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();// 2 计算桶的索引位置,从头节点开始遍历该条链表else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;do {// 3 将链表节点依次转为红黑树节点TreeNode<K,V> p = replacementTreeNode(e, null);// 4 若tl为null,代表初次循环,将头节点赋值给hdif (tl == null)hd = p;// 5 否则关联好当前节点的前后节点else {p.prev = tl;tl.next = p;}// 6 将p节点赋值给tl,用于下次循环关联操作tl = p;} while ((e = e.next) != null);// 7 将创建好的红黑树的头节点放入到桶的索引位置上,若头节点不为空,开始构建左右分支if ((tab[index] = hd) != null)hd.treeify(tab);}
}final void treeify(Node<K,V>[] tab) {TreeNode<K,V> root = null;// 1 将调用此方法的节点为起点开始遍历for (TreeNode<K,V> x = this, next; x != null; x = next) {// 下个节点next = (TreeNode<K,V>)x.next;// 将x的左右节点设置为空x.left = x.right = null;// 2 若没有根节点, 将x设为根节点if (root == null) {// 根节点没有parent节点x.parent = null;// 将根节点置黑(特性)x.red = false;// 将x设为根节点root = x;}else {// 去除x节点的key和hashK k = x.key;int h = x.hash;Class<?> kc = null;// 3 从根节点开始遍历for (TreeNode<K,V> p = root;;) {int dir, ph;K pk = p.key;// 4 如果x节点的hash值小于p节点的hash值,向左查找if ((ph = p.hash) > h)dir = -1;// 5 如果x节点的hash值大于p节点的hash值,向右查找else if (ph < h)dir = 1;// 6 比较后若hash值相等,则通过Comparable比较key值else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0)// 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找dir = tieBreakOrder(k, pk);TreeNode<K,V> xp = p;// 7 dir<=0 则向左查找,否则右查找,若为null代表该位置可插入,否则继续循环,p是变化的next节点if ((p = (dir <= 0) ? p.left : p.right) == null) {// x的父节点即为最后一次遍历的p节点x.parent = xp;// 如果时dir <= 0, 成为子左if (dir <= 0)xp.left = x;// 如果时dir > 0, 成为子右elsexp.right = x;// 9 执行一次红黑树平衡root = balanceInsertion(root, x);break;}}}}// 10 调整root节点到桶位的头节点上moveRootToFront(tab, root);
}

balanceInsertion方法

方法逻辑梳理:

  1. 插入节点x若没有父节点,则证明是根节点,置黑返回根节点。
  2. 插入节点x的父节点是黑色,代表当前平衡,直接返回,若其祖父节点为空,代表当前是第二层,直接返回根节点。
  3. 插入节点x的父节点是祖父节点的子左:
    • 右叔节点非空且呈红色,则叔、父节点置黑,祖父节点置红,起点x改为祖父节点重新开始循环。
    • 右叔节点为空或呈黑色,呈黑色代表平衡正在运行中。
      • 插入节点是子右,则父节点左旋(父节点下降,当前节点和父节点换了位置,且原父节点成为了子左)。
      • 父节点不为空则父节点置黑,祖父节点不为空则祖父节点置红,祖父节点右旋(祖父节点下降)。
  4. 插入节点x的父节点是祖父节点的子左:
    1. 左叔节点非空且呈红色,则叔、父节点置黑,祖父节点置红,起点x改为祖父节点重新开始循环。
    2. 左叔节点为空或呈黑色,呈黑色代表平衡正在运行中。
      1. 插入节点是子左,则父节点右旋(父节点下降,插入节点和父节点换了位置,且原父节点成为了子右)。
      2. 父节点不为空则父节点置黑,祖父节点不为空则祖父节点置红,祖父节点左旋(祖父节点下降)。
// root:根节点,x:新插入的节点
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,TreeNode<K,V> x) {x.red = true;// 定义变量,无限循环,只能从内部返回// xp:父节点,xpp:祖父节点,xppl:左叔节点,xppr:右叔节点for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {// 条件1:父节点是nil,说明插入节点就是根节点,置黑,直接返回根节点if ((xp = x.parent) == null) {x.red = false;return x;}// 条件2:父节点是黑色证明当前是平衡状态,置红,直接返回根节点,或祖父节点是nil,证明插入节点是第二层,直接返回根节点else if (!xp.red || (xpp = xp.parent) == null)return root;// 条件3:父节点是祖父节点的子左if (xp == (xppl = xpp.left)) {// 条件3.1:右叔节点非nil且呈红色,则叔、父节点置黑,祖父节点置红,起点改为祖父节点if ((xppr = xpp.right) != null && xppr.red) {xppr.red = false;xp.red = false;xpp.red = true;x = xpp;}// 条件3.2:右叔节点是nil或呈黑色,呈黑色代表平衡正在运行中else {// 条件3.2.1:插入节点是子右,则父节点左旋(父节点下降,当前节点和父节点换了位置,且原父节点成为了子左)if (x == xp.right) {root = rotateLeft(root, x = xp);xpp = (xp = x.parent) == null ? null : xp.parent;}// 条件3.2.2:父节点不为nil则父节点置黑,祖父节点不为nil则祖父节点置红,祖父节点右旋(祖父节点下降)if (xp != null) {xp.red = false;if (xpp != null) {xpp.red = true;root = rotateRight(root, xpp);}}}}// 条件4:父节点是祖父节点的子左else {// 条件4.1:左叔节点非nil且呈红色,则叔、父节点置黑,祖父节点置红,起点改为祖父节点if (xppl != null && xppl.red) {xppl.red = false;xp.red = false;xpp.red = true;x = xpp;}// 条件4.2:左叔节点是nil或呈黑色,呈黑色代表平衡正在运行中else {// 条件4.2.1:插入节点是子左,则父节点右旋(父节点下降,插入节点和父节点换了位置,且原父节点成为了子右)if (x == xp.left) {root = rotateRight(root, x = xp);xpp = (xp = x.parent) == null ? null : xp.parent;}// 条件4.2.2:父节点不为nil则父节点置黑,祖父节点不为nil则祖父节点置红,祖父节点左旋(祖父节点下降)if (xp != null) {xp.red = false;if (xpp != null) {xpp.red = true;root = rotateLeft(root, xpp);}}}}}
}

rotateLeft方法

该方法是红黑树平衡的核心方法,将树结构向左旋转。

这里旋转只分为两种不同情况:

  1. 情况一:旋转节点的子右的子左是空。
  2. 情况二:旋转节点的子右的子左非空。

else if (pp.left == p)该判断仅是旋转节点的父节点变了左指针或是右指针,不是关键,因此不纳入情况范围,同时if ((pp = r.parent = p.parent) == null)这个判断仅影响了父节点的颜色和是否为根,不是关键,因此也不纳入情况范围。

// root:根节点,p:要左旋的节点
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,TreeNode<K,V> p) {TreeNode<K,V> r, pp, rl;// 旋转节点与其子右非nilif (p != null && (r = p.right) != null) {// 旋转节点的子右的子左节点 成为 其子右,父认子if ((rl = p.right = r.left) != null)// 子认父rl.parent = p;// 旋转节点原子右 指向 其父节点,若父节点为nil,说明r已经root节点并置黑if ((pp = r.parent = p.parent) == null)(root = r).red = false;// 旋转节点是子左,其父节点的子左指向relse if (pp.left == p)pp.left = r;// 旋转节点是子右,其父节点的子右指向relsepp.right = r;// 旋转节点成为其原子右的子左r.left = p;// 旋转节点原子右成为其父节点p.parent = r;}return root;
}

核心:左旋情况一演示

核心:左旋情况二演示

rotateRight方法

该方法是红黑树平衡的核心方法,将树结构向右旋转。

这里旋转只分为两种不同情况:

  1. 旋转节点的子左的子右是空。
  2. 旋转节点的子左的子右非空。

else if (pp.right== p)该判断仅是旋转节点的父节点变了左指针或是右指针,不是关键,因此不纳入情况范围,同时if ((pp = l.parent = p.parent) == null)这个判断仅影响了父节点的颜色和是否为根,不是关键,因此也不纳入情况范围。

// root:根节点,p:要右旋的节点
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,TreeNode<K,V> p) {TreeNode<K,V> l, pp, lr;// 旋转节点与其子左不为nilif (p != null && (l = p.left) != null) {// 旋转节点的子左的子右 成为 其子左,父认子if ((lr = p.left = l.right) != null)// 子认父lr.parent = p;// 旋转节点原子左 指向 其父节点,若父节点为nil,说明r已经root节点并置黑if ((pp = l.parent = p.parent) == null)(root = l).red = false;// 旋转节点是子右,其父节点的子右指向lelse if (pp.right == p)pp.right = l;// 旋转节点是子左,其父节点的子左指向lelsepp.left = l;// 旋转节点成为其原子右的子右l.right = p;// 旋转节点原子左成为其父节点p.parent = l;}return root;
}

核心:右旋情况一演示

核心:右旋情况二演示

resize方法

扩容场景:

  • 桶位为空时,初始化扩容
  • 桶位被删光时,存放扩容
  • 桶位长度到达阈值时,触发扩容

方法逻辑梳理:

  1. 根据扩容场景以及当前桶的长度,重新定义新桶的长度和新桶的扩容阈值。
  2. 数据从旧桶迁移到新桶:
    1. 旧桶不为空,且是链表结构,执行链表迁移:

      1. 重新计算节点索引位置。
      2. 链表节点指针方向不变迁移(JDK1.7版本链表迁移是倒装的),部分节点可能迁移后索引位不变,部分节点可能迁移后索引位发生变化,这就导致一条链表被打散了。
    2. 旧桶不为空,且是红黑树结构,执行红黑树迁移
      1. 重新计算节点索引位置。
      2. 仅变幻红黑树的链表结构迁移。
      3. 部分节点可能迁移后索引位不变,部分节点可能迁移后索引位发生变化,这就导致一条链表被打散,打散后的链表头节点都要重新构建红黑树。
// oldCap:旧桶长度
// oldThr:旧桶扩容阈值
// newCap:新桶长度
// newThr:新桶扩容阈值
// loHead:扩容后,索引位不发生变化的临时头节点
// loTail:扩容后,索引位不发生变化的临时尾节点
// hiHead:扩容后,索引位发生变化的临时头节点
// hiTail:扩容后,索引位发生变化的临时尾节点
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 1 旧桶不为空的情况if (oldCap > 0) {// 1.1 判断旧桶长度是否超过最大容量值,若超过直接设置为Integer.MAX_VALUE,不扩容,返回旧桶if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 1.2 将新桶长度设为旧桶长度的2倍,若新桶长度 < 系统最大容量且旧桶长度 >= 16, 则将新桶扩容阈值设为旧桶扩容阈值的2倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}// 2 若旧桶长度为0, 但旧桶扩容阈值大于0, 则将新桶扩容阈值设为旧桶的扩容阈值else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;// 3 旧桶长度为0, 旧桶扩容阈值为0,则将阈值和长度设为系统默认值else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 4 若新桶扩容阈值为0, 则通过新桶长度 乘以 负载因子 获得新桶的扩容阈值,并判断是否超出系统默认最大容量if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}// 5 新桶的扩容阈值赋值给thresholdthreshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})// 根据新的容量,创建新桶Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 新桶赋值诶tabletable = newTab;// 6 若旧桶不为空,则遍历旧桶,将数据迁移到新桶if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;// 6.1 旧桶每个索引位的链表头节点,赋值给eif ((e = oldTab[j]) != null) {// 6.2 原旧桶的索引位置为null,方便垃圾回收oldTab[j] = null;// 7 如果e.next为空,则代表旧桶该链表只有一个节点,则计算新桶的索引位置,直接将该节点放在新桶的索引位上if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 8 若是红黑树节点,则进行红黑树的rehashelse if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 9 若是普通的链表,则进行链表的rehashelse { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 9.1 若e的哈希值与旧桶容量作与运算为0,则e的新索引位置与其在旧桶的索引位置相同if ((e.hash & oldCap) == 0) {// 若loTail为空, 代表当前的e是头节点,则将loHead赋值为头节点,否则放到队尾if (loTail == null)loHead = e;elseloTail.next = e;// 将loTail赋值为新增的节点,方便loTail.next = e作下节点的指向loTail = e;}// 9.2 若e的哈希值与旧桶容量作与运算非0,则e的新索引位置 = 其在旧桶索引位+旧桶长度else {// 若hiTail为空, 代表当前的e是头节点,则将hiHead赋值为头节点,否则放到队尾if (hiTail == null)hiHead = e;elsehiTail.next = e;// 将hiTail赋值为新增的节点,方便hiTail.next = e作下节点的指向hiTail = e;}} while ((e = next) != null);// 10 若loTail不为空,表明旧桶有索引位不变的节点放入到了新桶同样的索引位上,将loTail最后的指针指向null,并将loHead放入到新桶的对应索引位上if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 11 若hiTail不为空,标明旧桶的节点迁移到新桶的索引位为:原桶所在索引+旧桶长度,将hiTail最后的指针指向null,并将hiHead放入到新桶的计算后的新索引位上if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}// 12 返回新桶return newTab;
}// 扩容后,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置 + 旧桶长度
// map:当前的HashMap对象
// tab:新桶
// index:桶的索引位置
// bit:旧桶长度
// loHead:扩容后,索引位不发生变化的临时头节点
// loTail:扩容后,索引位不发生变化的临时尾节点
// hiHead:扩容后,索引位发生变化的临时头节点
// hiTail:扩容后,索引位发生变化的临时尾节点
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {// this是调用此方法的红黑树节点TreeNode<K,V> b = this;// Relink into lo and hi lists, preserving orderTreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null;int lc = 0, hc = 0;// 1 从this节点开始遍历红黑树for (TreeNode<K,V> e = b, next; e != null; e = next) {next = (TreeNode<K,V>)e.next;// 旧桶节点设为null,方便垃圾回收e.next = null;// 2 若e的哈希值与旧桶容量作与运算为0,则e的新索引位置与其在旧桶的索引位置相同if ((e.hash & bit) == 0) {// 若loTail为空, 代表当前的e是头节点,则将loHead赋值为头节点,否则放到队尾if ((e.prev = loTail) == null)loHead = e;elseloTail.next = e;// 将loTail赋值为新增的节点,方便loTail.next = e作下节点的指向loTail = e;// 统计原索引位的节点个数++lc;}// 3 若e的哈希值与旧桶容量作与运算非0,则e的新索引位置 = 其在旧桶索引位+旧桶长度else {// 若hiTail为空, 代表当前的e是头节点,则将hiHead赋值为头节点,否则放到队尾if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;// 将hiTail赋值为新增的节点,方便hiTail.next = e作下节点的指向hiTail = e;// 统计索引位为原索引位 + 旧桶长度的节点个数++hc;}}// 4若原索引位的节点不为空if (loHead != null) {// 4.1 若节点个数<=6个则将红黑树退化为链表if (lc <= UNTREEIFY_THRESHOLD)tab[index] = loHead.untreeify(map);else {// 4.2 将loHead设为新桶对应索引位的头节点tab[index] = loHead;// 4.3 若hiHead不为空,则代表原来的红黑树发生了变化(一个链表上的分散到了不同链表上),需要重新构建红黑树if (hiHead != null) // (else is already treeified)loHead.treeify(tab);}}// 5 若索引位为原索引位 + 旧桶长度的节点不为空if (hiHead != null) {// 5.1 若节点个数<=6个则将红黑树退化为链表if (hc <= UNTREEIFY_THRESHOLD)tab[index + bit] = hiHead.untreeify(map);else {// 5.2 将索引位为原索引 + 旧桶长度的节点设为新桶对应索引位的头节点tab[index + bit] = hiHead;// 5.3 若loHead不为空,则代表原来的红黑树发生了变化(一个链表上的分散到了不同链表上),需要重新构建红黑树if (loHead != null)hiHead.treeify(tab);}}
}// 红黑树退化成链表
final Node<K,V> untreeify(HashMap<K,V> map) {Node<K,V> hd = null, tl = null;// 1 从调用该方法的节点开始遍历,将所有节点转为链表节点for (Node<K,V> q = this; q != null; q = q.next) {// 2 调用replacementNode方法构建链表节点Node<K,V> p = map.replacementNode(q, null);// 3 如果tl为null,则代表当前节点为头节点,将hd赋值为该节点if (tl == null)hd = p;// 4 否则,将尾节点的next属性设置为当前节点elsetl.next = p;// 5 每次都将tl节点指向当前节点,即尾节点tl = p;}return hd;
}Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {return new Node<>(p.hash, p.key, p.value, next);
}

核心:扩容时旧链表的节点计算新的索引位的计算原理

(e.hash & oldCap) == 0 是计算新索引位置的判定条件,解释为原节点的哈希值同旧桶的长度作与运算,旧数组的数组长度是  ,公式即为 (e.hash & ) == 0 , 表示为是一个仅有一个比特位是1的二进制数,其他位都是0。若旧桶长度  比特位上是1的位置,且节点哈希值对应这个位置上的值不是1,那么运算结果一定是0。此时出现两种情况:

  1. 运算结果等于0:为0时,则该节点迁移时的索引位等于其旧桶所在的索引位。
  2. 运算结果不为0:为1时,则该节点迁移时的索引位等于其旧桶所在的索引位再加上旧桶的长度。

而这样设计的原因:

一条链表上的节点的key哈希值不一定都相等,而是 (n -  2)& hash 经过原哈希值取模之后桶位相等。已知hash是定量,n - 1 是变量,例如在桶长度为16时,只有低4位同key的哈希值作了与运算,高位都置为了0,那么扩容之后,n - 1 发生变化,那结果就不一定等于原来计算出的索引位(除非hash值正好某高几位是0与原结果相同),下图展示一次相同链表上的节点扩容后拆分的场景。

根据扩容后新桶长度时旧桶长度的2倍,则 (n - 1) & hash 的变种为(2n - 1) & hash 。

  • 以初始长度 n = 16 ,旧桶计算方式: (n - 1) & hash = 0000 0000 0000 1111 & hash
  • 以初始长度 n = 16 ,新桶计算方式: (2n - 1) & hash = 0000 0000 0001 1111 & hash

那么计算 (n - 1) & hash 时,新旧桶的区别就在于第5位比特位是0还是1,扩容后 (n - 1) 的第5位是1,若同时 hash 的第5位也是1,则第5位计算结果刚好是旧桶长度,仅需要旧桶索引加上旧桶长度就可得出新桶的索引位,其他情景桶扩容同理。而 e.hash & oldCap 就是为了证明第5位的计算结果是否为1。

remove方法

方法逻辑梳理:

  1. 检查:若桶不为空且长度不为0,且入参hash值对应索引位不为空。
  2. 检查:查找要删除的目标节点:
    1. 节点为链表类型。
    2. 节点为红黑树类型。
  3. 若是链表结构,则剔除链表节点,处理指针。
  4. 若是红黑树结构:
    1. 解除链表结构中的关联,同时检查树的节点数,节点过少则将红黑树退化回链表结构。
    2. 检查:若目标节点的子左和子右都不为空时。
      1. 找到置换节点(默认置换节点是查找目标节点子右的最深子左),与其交换颜色。
      2. 若置换节点是目标节点的子右,则两节点地理位置互换。否则,将置换节点的父节点设为目标节点的父节点,同时若置换节点的父节点不为空,将目标节点替换置换节点设为其父节点的子节点,同时目标节点的子右设为置换节点的子右,若目标节点的子右不为空,则将置换节点设为目标节点子右的父节点。
      3. 将目标节点的子左设为空。
      4. 若置换节点的子右不为空,将置换节点的子右设为目标节点的子右,同时将目标节点设为置换节点子右的父节点。
      5. 若置换节点的子左不为空,将目标节点的子左设为置换节点的子左,同时将置换节点设为目标节点子左的父节点。
      6. 若目标节点的父节点为空,将目标节点的父节点设为置换节点的父节点,同时将置换节点设为root根。否则,若目标节点是其父节点的子左,则将置换节点设为目标节点父节点的子左。否则,若目标节点不是root,且是其父节点的子右,则将置换节点设为目标节点父节点的子右。
      7. 若置换节点的子右不为空,则赋值给replacement变量,否则将目标节点赋值给replacement变量。
    3. 检查:若目标节点的子左不为空,replacement 赋值为其子左。
    4. 检查:若目标节点的子右不为空,replacement 赋值为其子右。
    5. 检查:若目标节点的子左子右都为空,replacement 赋值为目标节点。
    6. 最后移除操作一:目标节点不是叶子节点,使用之前赋值到replacement节点替换掉目标节点的位置,并将目标节点移除。
    7. 若目标节点非红色,则进行红黑树删除平衡调整,因为红色节点删除不影响树的平衡。
    8. 最后移除操作一:目标节点是叶子节点,直接移除目标节点。
    9. 将root移到桶索引位的头节点上。
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 检查:若桶不为空且长度不为0,且入参hash值对应索引位不为空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 若入参的hash值和key都与节点的相同,即为目标节点,赋值给nodeif (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;// 3 否则遍历next节点else if ((e = p.next) != null) {// 3.1 若目标节点是红黑树,则在树中查找if (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);// 3.2 否则在链表中查找else {do {// 入参的hash值和key都与节点的相同,即为目标节点if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}// 4 若node不为空证明找到了目标节点,开始操作移除if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {// 4.1 若目标节点是红黑树,则用红黑树方式移除if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);// 4.2 若目标节点是头节点,则直接将头节点改为目标节点的next节点else if (node == p)tab[index] = node.next;// 4.3 否则将目标节点的next节点,改为其父节点的next节点,完成移除elsep.next = node.next;++modCount;--size;// 供LinkedHashMap使用afterNodeRemoval(node);// 5 返回被移除的节点return node;}}return null;
}// 由于红黑树保留了链表结构,因此红黑树移除节点同样也涉及到链表的移除
// 步骤是:
//    1.先处理链表指针
//    2.再处理红黑树结构
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,boolean movable) {// -------- 处理链表指针 --------int n;// 1 检查:桶为空或桶长度为0,直接返回if (tab == null || (n = tab.length) == 0)return;// 2 计算索引位置int index = (n - 1) & hash;// 3 将索引位的头节点赋值给first变量和root根TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;// 4 将目标节点的next节点赋值给succ变量,其prev节点赋值给pred变量TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;// 5 若pred变量为空,则代表目标节点是头节点,则将其next节点放入头节点if (pred == null)tab[index] = first = succ;// 6 否则将目标节点的前节点的next节点,指向目标节点的next节点elsepred.next = succ;// 7 若succ变量不为空,则将succ的prev节点设置为pred(即目标节点next节点的前节点指向目标节点的前节点,相当于目标节点被剔除了)if (succ != null)succ.prev = pred;// 8 若first变量为空,代表该索引位没有节点,直接返回if (first == null)return;// 9 若root的父节点不为空,代表root发生变动了,重置rootif (root.parent != null)root = root.root();// 10 默认传过来的movable是true,若根节点为空 或 (根节点子右为空 或 根节点子左为空 或 根节点子左的子左为空),则会将红黑树退回到链表结构if (root == null|| (movable&& (root.right == null|| (rl = root.left) == null|| rl.left == null))) {tab[index] = first.untreeify(map);  // too smallreturn;}// -------- 处理红黑树 --------// 11 将目标节点赋值给p变量,其子左赋值给pl变量,其子右赋值给pl变量TreeNode<K,V> p = this, pl = left, pr = right, replacement;// 12 检查:若目标节点的子左和子右都不为空时if (pl != null && pr != null) {// 12.1 将目标节点的子右赋值给s变量TreeNode<K,V> s = pr, sl;// 12.2 从目标节点子右的左支链上一直翻找,直到最左深度赋值给s变量,s要么是目标节点的子右,要么就是其子右的最子左,这里先叫它置换节点while ((sl = s.left) != null) // find successors = sl;// 12.3 交换目标节点和置换节点的颜色boolean c = s.red; s.red = p.red; p.red = c; // swap colors// 置换节点的子右赋值给sr变量TreeNode<K,V> sr = s.right;// 目标节点的的父节点赋值给pp变量TreeNode<K,V> pp = p.parent;// 12.4 第一阶段处理:定位置换节点// 若置换节点是目标节点的子右,则两节点地理位置互换if (s == pr) { // p was s's direct parentp.parent = s;s.right = p;}// 否则else {// 置换节点的父节点,赋值给sp变量TreeNode<K,V> sp = s.parent;// 置换节点的父节点不为空 则设为 目标节点的父节点if ((p.parent = sp) != null) {// 若置换节点是其父节点的子左,则将目标节点 设为 置换节点的父节点的子左if (s == sp.left)sp.left = p;// 将目标节点 设为 置换节点的父节点的子右elsesp.right = p;}// 目标节点的子右 设为 置换节点的子右,若pr不为空,则将置换节点 设为 目标节点子右的父节点if ((s.right = pr) != null)pr.parent = s;}// 12.5 第二节点处理:置换节点与目标节点位置变动// 将目标节点的子左 设为 nullp.left = null;// 将置换节点的子右 设为 目标节点的子右,若sr不为空,则将目标节点 设为 置换节点子右的父节点if ((p.right = sr) != null)sr.parent = p;// 将目标节点的子左 设为 置换节点的子左,若pl不为空,则将置换节点 设为 目标节点子左的父节点if ((s.left = pl) != null)pl.parent = s;// 将目标节点的父节点 设为 置换节点的父节点,若pp为空,则将置换节点设为root根if ((s.parent = pp) == null)root = s;// 若目标节点是其父节点的子左,则将置换节点 设为 目标节点父节点的子左else if (p == pp.left)pp.left = s;// 若目标节点不是root,且是其父节点的子右,则将置换节点 设为 目标节点父节点的子右elsepp.right = s;// 12.6 若置换节点的子右不为空,则赋值给replacement变量,所以使用用其子右来 替换 目标节点的位置if (sr != null)replacement = sr;// 12.7 若置换节点的子右为空,则置换节点即为叶子节点,则将目标节点 赋值给replacement,只需将目标节点移除即可elsereplacement = p;}// 13 检查:若目标节点的子左不为空,replacement 赋值为其子左else if (pl != null)replacement = pl;// 14 检查:若目标节点的子右不为空,replacement 赋值为其子右else if (pr != null)replacement = pr;// 15 检查:若目标节点的子左子右都为空,replacement 赋值为目标节点elsereplacement = p;// 第三阶段处理:处理置换后的剩余指针,同时移除目标节点// 16 检查:若目标节点不是叶子节点,使用replacement节点替换掉目标节点的位置,将目标节点移除if (replacement != p) {// 16.1 将目标节点的父节点赋值给replacement节点的父节点TreeNode<K,V> pp = replacement.parent = p.parent;// 16.2 若目标节点的父节点为空,则将root节点赋值为replacement节点即可if (pp == null)root = replacement;// 16.3 若目标节点是其父节点的子左,则将目标节点父节点的子左赋值为replacementelse if (p == pp.left)pp.left = replacement;// 16.4 若目标节点不为root,且是其父节点的子右,则将目标节点父节点的子右赋值为replacementelsepp.right = replacement;// 16.5 目标节点的位置已经被replacement替换,将目标节点所有指向清空方便垃圾回收p.left = p.right = p.parent = null;}// 17 若目标节点非红色,则进行红黑树删除平衡调整,因为红色节点删除不影响树的平衡TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);// 18 检查:若目标节点为叶子节点,直接移除目标节点if (replacement == p) {  // detachTreeNode<K,V> pp = p.parent;// 18.1 将目标节点的父节点 设为 nullp.parent = null;// 若目标节点的父节点不为空if (pp != null) {// 18.2 若目标节点是其父节点的子左,则将父节点的子左指向 nullif (p == pp.left)pp.left = null;// 18.3 若目标节点是其父节点的子右,则将父节点的子右指向 nullelse if (p == pp.right)pp.right = null;}}// 19 将root移到桶索引位的头节点上if (movable)moveRootToFront(tab, r);
}final Node<K,V> untreeify(HashMap<K,V> map) {Node<K,V> hd = null, tl = null;// 循环每个节点,依次将所有节点链表化for (Node<K,V> q = this; q != null; q = q.next) {Node<K,V> p = map.replacementNode(q, null);if (tl == null)hd = p;elsetl.next = p;tl = p;}return hd;
}

核心:为什么当目标节点是红黑树时,置换节点的子右是替换目标节点的首选

代码中置换节点是 s 表示,置换节点的子右是 sr 表示,目标节点是 p 表示。

讲这里是以置换节点的子右不为空为前提,置换节点是目标节点的子右向左遍历的终点,此时的 s 已没有子左,而在判断是否用 sr 当作替换目标时,sr 在上部分已经完成了和目标节点的父子相认,成为了目标节点的子右,同时由于目标节点提前将子左置为了空,因此要移除目标节点 p,仅需将 sr 将其覆盖即可,按这种思路设计,sr 即是首选。

核心:红黑树移除节点演示

演示原始场景,目标节点 p 不是根节点 root,且有子左 pl 子右 pr,同时子右 pr 有最深子左 s,最深子左 s 也有子右 sr。

get方法

方法逻辑梳理:

  1. 检查桶:桶不为空,并且桶的长度大于0,并且要查找的键值对对应的节点不为空。
  2. 检查节点:节点哈希值和key是否和入参的相同,相同则头节点即为目标节点并直接返回。
  3. 若头节点不是目标节点,且有next节点则开始遍历链表查找,直接找到目标节点。
  4. 若是红黑树节点,则开始翻找红黑树:
    1. 红黑树根节点开始找,红黑树查找为深度优先,并根据入参的hash值大小来判断树枝的方向
    2. hash值小于遍历到的红黑树节点的hash值,从左子节点继续查
    3. hash值大于遍历到的红黑树节点的hash值,从右子节点继续查
    4. 红黑树没有目标节点,返回null
  5. 链表没有目标节点,返回null。
public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 1 检查桶:桶不为空,并且桶的长度大于0,并且要查找的键值对对应的节点不为空if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 2 检查节点:节点哈希值和key是否和入参的相同,相同则头节点即为目标节点并直接返回if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// 3 若头节点不是目标节点,且有next节点则开始遍历链表查找if ((e = first.next) != null) {if (first instanceof TreeNode)// 4 若是红黑树节点,则翻找红黑树return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {// 5 遍历链表,直至找到目标节点并返回if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}// 6 没有目标节点,返回nullreturn null;
}final TreeNode<K,V> getTreeNode(int h, Object k) {return ((parent != null) ? root() : this).find(h, k, null);
}final TreeNode<K,V> find(int h, Object k, Class<?> kc) {// h:插入键hash值,k:插入键, kc:插入键class对象// 1 调用此方法的节点赋值给pTreeNode<K,V> p = this;// 2 从p节点开始向下遍历do {int ph, dir; K pk;TreeNode<K,V> pl = p.left, pr = p.right, q;// 3 插入的hash值小于p节点的hash值,向左遍历if ((ph = p.hash) > h)p = pl;// 4 插入的hash值大于p节点的hash值,向右遍历else if (ph < h)p = pr;// 5 插入的key与其hash值等于p节点的key与hash值,直接返回p节点else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;// 6 p节点的子左为空,向右遍历else if (pl == null)p = pr;// 7 p节点的子右为空,向左遍历else if (pr == null)p = pl;// 8 若比较的hash值相等,则根据comparable方式比较大小else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)p = (dir < 0) ? pl : pr;// 9 若key所属类未实现Comparable, 直接指定向右遍历else if ((q = pr.find(h, k, kc)) != null)return q;// 10 向右遍历为空, 改为向左遍历elsep = pl;} while (p != null);return null;
}

手撕HashMap数据结构(带你逐行阅读源码)相关推荐

  1. 源码面前没有秘密,推荐 9 个带你阅读源码的开源项目

    在文章开始之前,请各位先回忆下在日常开发过程中,都使用或依赖了哪些开源项目?是不是发现,开源项目已经完全融入到日常开发! 如今大多数的程序员技术栈和工具箱里,或多或少都有开源项目的身影:大到操作系统. ...

  2. Java外卖点餐送餐平台源码带手机端带文档(源码分享)

    Java仿饿了么外卖点餐送餐平台源码带手机端带文档(源码分享) 一个简单的外卖系统,包括手机端,后台管理,api基于spring boot和vue的前后端分离的外卖系统.包含手机端,后台管理功能. 核 ...

  3. 【阅读源码系列】ConcurrentHashMap源码分析(JDK1.7和1.8)

    个人学习源码的思路: 使用ctrl+单机进入源码,并阅读源码的官方文档–>大致的了解一下此类的特点和功能 使用ALIT+7查看类中所有方法–>大致的看一下此类的属性和方法 找到重要方法并阅 ...

  4. hashmap实现原理_Java中HashMap底层实现原理(JDK1.8)源码分析

    在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依 ...

  5. Java面试绕不开的问题: Java中HashMap底层实现原理(JDK1.8)源码分析

    这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一样,原来他们没有指定JDK版本,很多文章都是旧版本JD ...

  6. 阿里开发者们的第16个感悟:让阅读源码成为习惯

    2015年12月20日,云栖社区上线.2018年12月20日,云栖社区3岁. 阿里巴巴常说"晴天修屋顶". 在我们看来,寒冬中,最值得投资的是学习,是增厚的知识储备. 所以社区特别 ...

  7. 阅读源码的 4 个绝技,我必须分享给你!

    为什么要阅读源码? 1.在通用型基础技术中提高技术能力 在 JAVA 领域中包含 JAVA 集合.Java并发(JUC)等, 它们是项目中使用的高频技术,在各种复杂的场景中选用合适的数据结构.线程并发 ...

  8. 丁威: 优秀程序员必备技能之如何高效阅读源码(二更)

    @[toc](丁威: 优秀程序员必备技能之如何高效阅读源码(二更)) 消息中间件 我能熟练使用这个框架/软件/技术就行了, 为什么要看源码?" "平时不用看源码, 看源码太费时间, ...

  9. 阅读源码不得不知道的事

    碎碎念 好哥哥们,整个 Redis 系列到这就算完结了.虽然没有到阅读源码那么深,但是很多知识点的原理基本上都是通过画图的方式讲清楚了(源码主要也看不懂啊,基本的一些语法都忘光了).那接下来的话方向还 ...

最新文章

  1. android可点击的列表,如何在Android的可扩展列表视图中的子点击...
  2. EtherCAT伺服驱动器-如何选择硬件开发方案
  3. Lua学习教程之 可变參数数据打包与解包
  4. 自适应来电模拟器微信小程序源码 可自定义来电名称归属地铃声等
  5. linux系统日志_Linux系统学习系列——Linux系统日志管 理(下 )
  6. 令人厌恶的错误MSB3721,以及win10,VS2019,YOLO V4 环境搭建
  7. 绝对定位实现漂浮工具条停靠在内容旁边
  8. 栈 -- 以及用栈实现计算器
  9. 圆角正方形 html,ps正方形角怎么变圆角 ps怎么在原来的矩形中改成圆角
  10. 交通大数据干货总结(1)
  11. 物联网发展跨越拐点!2020 AIoT产业年终盛典圆满落幕
  12. 基于WordPress搭建个人网站
  13. ImportError: sys.meta_path is None, Python is likely shutting down 解决方案
  14. Java多态性:Java什么是多态?
  15. l1-047. 装睡c语言,L1-047 装睡 (10 分)- PAT团体程序设计天梯赛
  16. 电话录音系统服务器 显示断线,申瓯电话录音系统常见问题处理
  17. 2019年淘宝运营里中小卖家需要掌握的技能!
  18. 折现分割平面(递推)
  19. 【数据结构】各种数据结构的特点介绍
  20. 静态网页-猫眼电影首页完整版(附源码)

热门文章

  1. 四川小学计算机的组成是几年级学,小学三年级计算机教案
  2. Error:Execution failed for task ':recordlib:lint'. Lint found errors in the project; aborting buil
  3. 华为防火墙企业双出口专线,配置策略路由实现多个ISP出接口的智能选路和双向NAT
  4. jack分享的1-3开wifi 零火版本智能开关解决方案
  5. 图的深度(DFS)/广度优先搜索算法(BFS)/Dijkstra
  6. ssm基于微信小程序的恋上诗词设计与实现毕业设计源码011431
  7. thinkpad笔记本鼠标指针一直往左下角滑动解决方法
  8. 20天从入门到项目实战:学习小组C1任务训练实录
  9. 数学归纳法及例题分析
  10. 制作像UberEats和Deliveroo这样的移动应用程序需要多少钱