概述
HashMap 作为平时开发过程中常见的数据结构经常被用到,网上关于它的博文已经有非常多,为了加深我对它的理解,我计划就 JDK1.8 版本,通过源码的形式整理下它的原理。
HashMap
HashMap在源码中是这样定义的:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable,Serializable
这里我通过简单类图描述一下这几个类之间的关系:
-
Map 接口主要声明一些常用的 key-value 接口方法
-
AbstractMap 抽象类实现 Map 接口,它实现了部分接口方法,以及创建静态内部类 SimpleEntry。
-
Cloneable 接口主要和 clone() 方法有关,实现该接口可以重写 clone() 方法
-
Serializable 主要和序列化有关,实现该接口可以让对象序列换
关于 SimpleEntry 类这里我们先不详细说明,等后面用到了我们再说
构造方法
HashMap 类的构造方法主要有以下几种:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
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);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
其中前三种构造方法比较常见,第四种构造方法用的比较少,这里我们主要以常用的前三种为主。
通过源码我们可以看出,前三种构造方法主要是为了初始化属性 loadFactor 和 threshold 的值。这里我直接给出结论:
-
loadFactor:hashMap 的负载因子,当集合中元素个数达到负载因子所对应数量后 hashMap 会 扩容,通过源码可以看出默认的负载因子是0.75
-
threshold:hashMap 所对应管道数,每个管道可以保存多个元素
下面我们看一下 threshold 属性是如何计算出来的,即 tableSizeFor() 方法的源码:
static final int MAXIMUM_CAPACITY = 1 << 30;
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;
}
通过该方法可以获取大于等于参数的最小的2的幂次方:也就是说,如果参数是5,输出8,参数是9,输出16,每次都输出大于当前参数的最小二次幂。
从这里也就可以看出,hashMap 的管道个数总是2的幂次方,关于这样做的原因后面根据源码具体分析。下面我们看一个草图,通过这个草图对 hashMap 有一个大致的认识:
put() 方法
有了上面的铺垫,下面我们具体看一下 hashmap 是如何保存一个元素的:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict){
// 暂时先省略,下面着重解决
}
这里我先给出 putVal() 方法这五个参数所代表的意义:
- hash:key 的 hash 值
- key :要保存的 key
- value:要保存的 value
- onlyIfAbsent:是否修改已经存在的数据,如果该参数为 true,则不会修改已经存在的key
- evict:该参数在 hashMap 中没有用到,在 hashMap 的子类 linkedHashMap 有用到,如果该参数为true,在添加元素后可能会删除顶部元素。
接下来我们看一个 hash() 方法的实现:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
该方法只是将 key 的 hashCode() 值散列的高位向地位移动一下。
Node内部类
在正式开始阅读 putVal() 源码前,我们先看看内部类 Node 的源码,该类是 putVal() 方法的基础:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 省略 get() set()
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
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;
}
}
通过源码我们可以看出,Node 内部保存 key、value 、hash 值以及指向下一个节点的 next 引用。其中它重写了 hashCode() 方法 和 equals() 方法。关于为啥重写这两个方法可以点击这里参考我之前的博客。
其实看到这里 hashMap 的原理基本上已经可以猜出来了,只需要把上面的草图改成下面这样:
也就是说,hashMap 实际上就是通过 Node 内部类组成的数组和链表实现的
putVal() 源码
有了上面这些基础,我们具体来看 putVal() 方法具体是怎么做的。在下面源码中我尽量通过注释的形式介绍,部分特别重要的内容附加在源码后面:
// transient 表示该属性不会序列化,这里table就表示上图中的数组块内容
transient Node<K,V>[] table;
// 单个链表的阈值
static final int TREEIFY_THRESHOLD = 8;
// 数组中工作节点个数
transient int modCount;
// 当前hashMap存储的节点个数
transient int size;
// hashMap 所能容纳的最大节点个数
int threshold;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 说明数组还没有初始化,即 hashmap 第一次添加元素时
if ((tab = table) == null || (n = tab.length) == 0)
// resize()方法初始化数组,关于该方法的源码下面我会给出,这里的n表示数组的长度,可以理解为hashMap的管道数
n = (tab = resize()).length;
// 判断管道头是否为空,如果为空,直接将要put的元素添加到数组对应下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 判断数组元素key是否等于要put的元素key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断是否使用TreeNode
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 从链表头依次判断是否存在节点key与参数相等
for (int binCount = 0; ; ++binCount) {
// 已经遍历到链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果某个链表的节点个数达到阈值
if (binCount >= TREEIFY_THRESHOLD - 1)
//
treeifyBin(tab, hash);
break;
}
// 判断是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
// 这里的 e可能是新添加的Node节点,也可能是老的Node节点(hash相等,key相等情况)
V oldValue = e.value;
// 更新新值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 该方法在hashMap为空,什么都没做
afterNodeAccess(e);
// 返回给节点对应的老值
return oldValue;
}
}
// 工作节点个数加一
++modCount;
// 判断hashmap元素数是否大于阈值
if (++size > threshold)
// 扩容
resize();
// 该方法在 hashmap 为空
afterNodeInsertion(evict);
return null;
}
上述代码中,我主要提一下这行代码:
p = tab[(n - 1) & hash]
从这里我们可以看出,元素属于哪个数组节点是由它的hash值和数组长度决定的。元素的hash值是随机的,数组的长度是确定的,为了让元素尽可能平均的分配到所有节点,(n-1) & hash 的计算结果必须尽可能的均匀。
当元素的长度总是2的幂次方时,n-1的值转化为二进制总是111…1。在这种情况下,随机的hash值计算出的结果也就相对比较均匀,这也是为什么 hashmap 中数组的长度总是2的幂次方的主要原因。
-
为什么要让元素分配的相对均匀呢?
元素在数组上分配均匀无论是添加还是修改或是查询,都只需要遍历较少的节点数量,这对于效率的提升至关重要。
hashmap 的初始化及扩容
在整理 putval() 方法时,我们提到 hashmap 是在第一次put元素时初始化,当元素数量超过阈值时,扩容也是调用该方法,下面我整理一下 resize() 方法的源码:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 原先的数组大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 原先hashmap所能容纳的最大元素数
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 数组已经达到最大值
if (oldCap >= MAXIMUM_CAPACITY) {
// 将最大元素数设置为 MAX
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 数组长度扩容为原来的两倍,如果hashmap最大容量超过16,也变为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
// 如果数组长度为空,最大元素数不为0,将数组长度设置为容量大小
else if (oldThr > 0)
newCap = oldThr;
// hashmap还未初始化时,此时容量以及数组大小都为0
else {
// 默认数组大小为16
newCap = DEFAULT_INITIAL_CAPACITY;
// 默认hashmap阈值为 16 * 0.75(负载因子)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果此时hashmap容量为0
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"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果是数组扩容,重新分配所有元素
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 数组节点只有一个元素时的情况
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 存的是TreeNode时的情况
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
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;
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;
}
上述代码中,我主要提一下 hashmap 扩容时元素重新分配的原理:
-
如果某个数组节点对应的链表只有一个元素,直接根据新的数组长度计算
-
如果某个数组节点对应的链表有多个元素,则会根据一个 奇妙的算法计算
首先,hashmap 每次扩容时,数组长度都会变为原来的 2倍。转化为二进制就可以这样表示:
而计算元素属于哪个数组下标是和数组长度减1来计算的,数组长度减1转二进制分别对应:
- 原来的数组长度转二进制:N 个 “1”
- 现在的数组长度转二进制:N + 1 个 “1”
也就是说,通过新数组长度所计算出的下标,其实就是多算了一位第 n+1 位的 “1”,举个例子:
hash值等于10的元素转二进制 -> 1010,将它保存到长度为2的数组
此时属于哪个下标 -> 1010 & 0001,也就是说只看最后一位
如果数组长度扩容为原来的二倍
此时属于哪个小标 -> 1010 & 0011,也就是说,在原来的基础上,往前多判断一位
写成公式:扩容后的下标 = 扩容前下标 + 新加位是否为1
为了判断这个新加位是否为1,只需要让hash值直接和原数组长度进行&运算即可,
也就是上面代码所对应的 e.hash & oldCap
这个新加长度所对应的值,实际上也就是 oldCap
总结一下,数组扩容后,当前元素要么属于原来的数组下标,要么属于原来的数组下标加上原数组长度
Node 转 TreeNode
在整理 putVal() 方法时,当单个链表元素数量超过链表阈值时会调用 treeifyBin() 方法,在正式学习treeifyBin() 方法源码前,我们先看看内部类 TreeNode 的结构:
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;
// 省略部分实现好的内部方法
}
从该对象的属性很明显可以看出,它是一种树形结果,并且是性能较好的 红黑树。而 LinkedHashMap.Entry 又是 Node 的子类,也就是说,该TreeNode 也是 Node 的子类,通过这种方式实现了 Node 转 TreeNode 的可能性,因为对象可以 向上转型。
下面我们具体看 treeifyBin() 方法的源码:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 默认情况下只会初始化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
// Node 转 TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
该方法是在某个链表超过阈值时,将所有Node节点转换为TreeNode节点,并通过前驱、后缀引用连接起来,下面我们主要看一下 treeify() 方法的源码:
final void treeify(Node<K,V>[] tab) {
// 创建头节点
TreeNode<K,V> root = null;
// 循环当前链表节点
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
// 初始化头结点
if (root == null) {
x.parent = null;
// 表明当前节点是“黑节点”
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 根据 hash 值向左向右遍历
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// 如果hash值相等,根据comparable接口方法判断是否相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// 记录当前节点,表示要判断节点的父节点
TreeNode<K,V> xp = p;
// 只有进入该方法,才表示找的真正的落点,进行保存
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 重新让树表的平衡
root = balanceInsertion(root, x);
break;
}
}
}
}
// 确定数组对应节点是红黑树的头节点
moveRootToFront(tab, root);
}
该方法表示当某个链表长度超过阈值时,将链表转换为红黑树的结构,提高效率。关于红黑树的结构我们后面做专门介绍(内容实在太多),就不再这里深入方法本身做探讨了。
get() 方法
看过put()方法的源码, get() 方法就简单了很多,下面我们直接看源码:
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;
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;
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;
}
看过 put() 方法源码后,看 get() 方法简直不要太轻松,几乎没有什么新内容,这里我们简单描述一下如果链表结构为 TreeNode 红黑树时,如何遍历。具体我们看代码:
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 这里主要保证每次遍历总是从最高级节点开始,root() 方法只是循环遍历到父节点
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 向左遍历
if ((ph = p.hash) > h)
p = pl;
// 向右遍历
else if (ph < h)
p = pr;
// hash 相等且 key 相等,说明就是当前节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// hash 相等,但是左节点为空时,向右遍历
else if (pl == null)
p = pr;
// hash 相等,但是右节点为空时,向左遍历
else if (pr == null)
p = pl;
// kc为 true 时,根据 compare 方法判断,默认为false
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 根据左节点查询
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 根据右节点查询
else
p = pl;
} while (p != null);
return null;
}
总结一下:当链表转换为红黑树结构后,通过hash值作为参考,key值作为一锤定音的判断来实现的。当hash值相等时,根据对象的 compare() 方法做判断
putTreeVal() 方法
有了上面红黑树的基础,我们再来看看,当链表结构转化为红黑树结构后,如何添加新元素。具体我们看源码:
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 定位到最顶级父节点
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// compare() 方法相同只会遍历一次
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
// 向左查,向右查,有值就返回
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
// 通过 compareTo 方法和 identityHashCode() 确定方向
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 将重新平衡后的红黑树的头结点设置在数组中
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
从源码可以看出,当链表转换为红黑树后,添加元素的逻辑实际上和转化时大致相同。
红黑树的扩容
上面我们讲了链表的扩容方法,这里我们主要看看当链表转换为红黑树后,整个红黑树的扩容方法,即 split() 方法的源码:
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
// 遍历整个红黑树,将整个红黑树改为两个链表,一个链表记录新增位 & 为0,表示扩容后还在当前下标,另一个记录新增位 & 为1,表示扩容后再原下标+老数组长度下标
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
// 处理对应数组下标不会变的情况
if (loHead != null) {
// 如果长度小于6,TreeNode 转为 Node
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
// 红黑树化
loHead.treeify(tab);
}
}
// 处理数组下标变为原下标加原数组长度的情况,具体逻辑同上
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
总结以下:红黑树的扩容就是遍历整个红黑树,将结果转为两个链表,一个链表记录下标会变的情况,另一个链表记录下标不会变的情况,根据链表的长度,决定是否将链表转Node链表,还是红黑树化。
红黑树的平衡
在链表处理转红黑树或给红黑树添加新元素后,都会执行 balanceInsertion() 方法平衡红黑树,下面我们来看一下它的源码:
// root 表示红黑树的根节点,x表示最后一个添加的节点
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 表示 x 就是root节点,直接返回
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 如果x的父节点就是黑色,并且它没有父节点,直接返回root,表示只有两个节点,root就是根节点
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 如果x的父节点是它自身父节点的左子树
if (xp == (xppl = xpp.left)) {
// x 的父节点的父节点的右子树不为空
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// x 的父节点的父节点的右子树为空
else {
// 判断 x 是否它父节点的右子树
if (x == xp.right) {
// 向左旋转x的父节点
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
// 向右旋转
root = rotateRight(root, xpp);
}
}
}
}
// 如果是右子树,下述情况和上面对称
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
上述代码本身比较复杂,主要涉及红黑树的左旋、右旋以及颜色的处理。关于这块我暂时也看的迷迷糊糊,等后续整理红黑树时着重整理,我暂时先给出两个关于左旋以及右旋的示意图:
如上图所示,就可以让树变得相对比较平衡。关于红黑树的只是暂且整理到这里,后面加上颜色着重分析。
关于 hashmap 的源码我就整理到这里,对于一些其他方法的实现,我想如果你读懂了 put() 以及 get() 方法,那基本不会有啥难度。