原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/10022092.html
Java基础系列-HashMap 1.8
概述
HashMap是基于哈希表实现的映射集合。
HashMap可以拥有null键和null值,但是null键只能有一个,null值不做限制。HashTable是不允许null键和值的。
HashMap是非线程安全的集合,HashTable是添加了同步功能的HashMap,是线程安全的。
HashMap是无序的,并不能保证其内部键值对的顺序。
HashMap提供了常量级复杂度的元素获取和添加操作(当然是在hash分散均匀的情况下)。
HashMap有两个影响功能的因素:初始容量与负载因子,当集合中的元素数量超过了初始容量和负载因子的乘积值时,会触发resize扩容
HashMap默认的初始容量是16,负载因子是0.75
HashMap在链表添加元素是采用尾插法,之前的版本采用头插法,因为会导致循环链表的问题,改成了尾插法,并添加了红黑树来优化链表过长的情况下查询慢的问题
HashMap底层结构为数组+链表/红黑树
HashMap底层链表的元素达到8个的情况下,如果HasnMap内部桶个数(即桶容量)达到64个则进行树形化,否则进行resize扩容
常量/变量解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
// 默认的初始容量,值为16,如果自定义也必须为2的次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量值
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子,值为0.75,可自定义,必须小于1
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转树结构的元素数量界限值,当某个hash值下的链表元素个数达到8个,
// 则将其改为树结构
static final int TREEIFY_THRESHOLD = 8;
// 红黑树反转链表的元素数量界限值,当数量小于6的时候才会反转
static final int UNTREEIFY_THRESHOLD = 6;
// 树形阈值,这个阈值针对的是整个Map中桶的数量,表示只有在所拥有的桶数量
// 达到64时才能执行树形化,否则先去扩容去吧,可见在桶数小于64时,优先执行扩容
static final int MIN_TREEIFY_CAPACITY = 64;
// 表示桶数组
transient Node[] table;
// 缓存
transient Set> entrySet;
// 集合中元素的数量,
transient int size;
// 集合结构的修改次数,包括集合元素的增删,和集合结构的变化,仅仅更改已有
// 元素的值并不会增加该值,主要用于Iterator的快速失败
transient int modCount;
// 集合的扩容阈值
int threshold;
// 集合的负载因子,默认0.75,时间与空间的折中,增加负载因子,
// 能增加元素容纳量,减小空间消耗,却增加的查询的时间消耗,
// 减小负载因子,能减少元素容纳量,减少查询时间消耗,但却要及早的去扩容,
// 增加了空间消耗
final float loadFactor;
// ...
}
功能解析
添加元素操作
功能描述:
添加新的映射元素(newKey,newValue),首先通过特定的hash算法计算newKey的hash值(newHash)。
Hash算法:获取newKey的hashCode值,然后进行高低位相异或。
hashCode值的获取方法在Object类中已有定义,当然也有某些类进行了重写,总的来说有以下几种:
- String类型的hashCode:自定义算法较复杂
- 包装类型的hashCode:当前值
- 其他类型的hashCode:类名+@+内存位置的16进制表示
如果是首次添加元素,那么就意味着桶尚未初始化,所以这里会先执行初始化操作(resize),
如果初始化成功或者非首次添加元素,那么开始定位元素的桶位
桶定位算法:用之前hash算法的结果newHash与桶的个数-1进行与操作
该算法的本意是保留newHash值的后几进制位来确定桶位,如何保留后几位呢?我们知道二进制算法中1的与操作具有保留原值的效果
这里正是使用这种原理来实现,假设桶位数为16位,16的二进制位10000,16-1=15,15的二进制位就是1111,末四位全是1,通过1的
保留原值的作用,当那它与newHash值的二进制值进行与操作后,结果就是newHash保留后4位的结果。而4位正好在桶位0-15之内。
而这也就是桶位数必须是2的次幂的原因,因为2的次幂的数字的二进制值全部是首位为1,其后全是0的值,当其-1之后就会变成首位
变0,其后全是1的值,而桶的下标是从0开始,最高位正好是-1之后的值。
查看确认桶位是否已有元素,如果没有,直接存放新元素到该桶位,
如果桶位已有元素存在,那么就是出现碰撞,这时的解决办法就是使用链表或者红黑树来存储,
如果该桶位存储的数据结构已经是红黑树,那么执行红黑树添加元素操作,
否则执行链表的后插法,将新元素插入到链表的末尾。
后插法:1.7之前的版本全是前插法,将新元素作为表头元素,1.8之后改成后插法,将新元素作为表尾元素
在执行后插法的时候需要遍历链表,查找是否存在相同key的元素,若存在则直接用newValue替换旧值,不再执行插入操作。
新元素插入完成之后,校验Map中总元素个数是否达到了阈值(这个阈值是桶容量和负载因子的乘积),如果超过阈值则进行扩容。
源码解析:
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
public V put(K key, V value) {
// 首先通过hash方法计算hash值,然后执行存值操作
return putVal(hash(key), key, value, false, true);
}
// hash算法:首先获取key的hashCode,然后将其高低16位相异或,全员参与(hashcode值的所有二进制位都能参与hash运算)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 首次添加值执行初始化
n = (tab = resize()).length;
// 定位桶下标,n的值为2的次幂,同时也是桶的数量,hash是之前通过hash算法得出的结果,n-1之后末几位全部是1,
// 再和hash与运算,等于保留hash的后几位作为结果。比如:(1111)&(01010101)的结果为0101,保留了后四位
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 存在相同key的情况(桶位置)
e = p;
else if (p instanceof TreeNode)
// 红黑树的情况
e = ((TreeNode)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);
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))))
// 存在相同key的情况(链表元素位置)
break;
p = e;
}
}
// 针对存在相同key的情况进行统一处理:替换value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();// 扩容两倍
afterNodeInsertion(evict);
return null;
}
//...
}
注意:桶容量为2的次幂的原因正是因为便于元素通过位运算实现定位。
初始化/扩容操作
功能描述
执行扩容方法的原因主要是集合中元素数量达到阈值或者是集合桶数组某个桶位置的元素数量达到8个,但集合桶容量未超过64的情况下,
特殊的情况是首次添加元素时的初始化操作也走这个方法。
针对初始化操作:
只会计算初始化容量和初始化阈值然后创建一个初始桶数组并返回结果。
对于使用了带参构造器的情况,会定制初始容量和负载因子,如果只定制了初始容量则使用默认负载因子,
构造器会通过一个进制运算根据自定义的容量算出一个大于等于自定义容量值的最小的2的次幂值作为真正的容量
比如:自定义容量为10,则计算容量值为16,然后再根据这个容量计算阈值为12。
而针对扩容操作:
首先校验旧容量是否已经达到或者大于容量最大值MAXIMUM_CAPACITY,如果是则不再进行扩容操作,还在原桶数组中保存元素,
并将阈值设置为Integer的最大值,设置为最大值之后就不会再触发扩容操作(因为Map中元素的总个数最大也就是Integer的最大值了,不可能比之更大),
然后校验容量加倍后的新容量是否超过容量最大值MAXIMUM_CAPACITY,如果没有的话则将阈值加倍。
新容量和新阈值都有了,然后创建新的桶数组,在之后就是元素迁移了。
元素迁移:
遍历旧桶数组,校验每个桶位的元素结构,
如果只有一个元素,直接在新桶数组进行重定位,定位方式不变,
如果是红黑树,走树结构迁移逻辑,
否则就是链表,进行链表迁移,链表迁移进行了平衡优化,由于新桶数组和旧数组的两倍容量,
我们简单的将新容量分成相等的两半,称之为低位区与高位区,低位区下标与旧数组相同,
高位区下标为旧数组下标+旧数组容量。
链表迁移时,会根据该链表中元素的键的hash值与旧容量进行与运算,这就会有两个结果,为0或者不为0。
旧容量也是2的次幂,高位为1,其后全是0,比如10000(表示容量为16),将其和hash结果相与,
只会保留旧容量二进制为1的那一位对应的hash值的那一位,其余位全变成0,如果hash值的那一
位为0结果就是0,hash值那一位为1结果就是10000。
根据相与的结果来进行链表分拆,将结果为0的链表元素还定位到相同的桶下标位,即新桶数组的低位区,将结果为1的链表元素定位到原下标+旧桶容量的位置,即高位区。
这两个链表会先组装链表结构,然后将链表表头元素定位到低位区或高位区。
源码解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;// 旧容量
int oldThr = threshold;// 旧阈值
int newCap, newThr = 0;// 新容量、新阈值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果旧容量已经达到最大容量值,将阈值设置为最大值,返回旧桶数组
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 如果加倍后的新容量没有超过最大容量,且旧容量大于等于16,则新阈值加倍
newThr = oldThr << 1; // double threshold
}
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);
}
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[] newTab = (Node[])new Node[newCap];// 创建桶数组
table = newTab;
// 如果是初始化操作,此处oldTab为null,会直接返回新建桶数组,否则执行元素迁移
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 针对桶位置只有一个元素的情况,直接重定位元素,定位模式一致
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 针对红黑树的情况
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order 针对链表的情况
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {// 遍历旧桶位的旧链表
next = e.next;
// 这个判断的结果取决于在于hash值在于oldCap的1所在进制位对应的进制位是1还是0,
// 由于oldCap只有这一位为1,那么hash的该位将保留原值,其余位全部得0,增加这么
// 一个貌似随机的判断,用于进一步分散元素到不同的桶。
// 其实就是将旧桶第i位桶的链表元素分散到新桶的第i和第i+oldCap桶位上,为0还是为1随机
// 在循环中形成两个小链表,然后将首个元素赋值给新桶的对应桶位即可。
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;
}
//...
}
获取元素操作
操作描述
简单看代码
源码解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
public V get(Object key) {
Node e;
// 计算key的hash值用作桶定位的基础
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
Node[] tab; Node 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)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;
}
//...
}
移除元素操作
操作描述
简单看源码
源码解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
public V remove(Object key) {
Node e;
// 计算key的hash值,用作后面定位元素桶位的基础
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
// 先进行桶定位,定位方式不变
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node node = null, e; K k; V v;
// 针对只有一个元素、链表头元素、树根元素进行处理
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 如果第一个元素不是,针对链表和树结构进行后续元素处理
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 执行树结构获取元素逻辑
node = ((TreeNode)p).getTreeNode(hash, key);
else {
// 链表循环获取等key的元素
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
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)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;
}
//...
}
红黑树
树形化操作
操作描述
参照源码
源码解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
// 树形化准备
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 对于触发了树形化操作,但是桶容量还没达到64的情况下优先去做扩容处理,扩容也会分拆链表
resize();
// 定位要做树形下的桶位置,获取桶位元素e
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode hd = null, tl = null;
// 循环遍历链表中的元素,将其改造成为双向链表结构,表头元素为hd
do {
// 将e元素封装成为树节点TreeNode
TreeNode 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 replacementTreeNode(Node p, Node next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
static final class TreeNode extends LinkedHashMap.Entry {
//...
// 树形化操作
final void treeify(Node[] tab) {
TreeNode root = null;//代表根节点
// 此处循环将this赋值给x,this代表的是当前树节点,这个类是HashMap的内部类用于标识树节点,
// this就是当前类的实例,也就是一个树节点,但是是哪个树节点,就要依靠之间的代码上下文来判
// 断了,看看调用该方法的地方有这样的代码:hd.treeify(tab);这就表示当前节点就是那额hd节
// 点,而这个hd节点就是之前改造好的双向链表的表头结点
// 这里循环的是双向链表中的元素
for (TreeNode x = this, next; x != null; x = next) {
next = (TreeNode)x.next;
x.left = x.right = null;
if (root == null) {
// root == null的情况是链表头结点的时候才会出现,这时候将这个头结点作为树根节点
x.parent = null;//根节点无父节点
x.red = false;//黑色
root = x;//赋值
}
else {
// 这里只有非链表头节点才能进来
K k = x.key;
int h = x.hash;
Class> kc = null;
// 此处循环的是已构建的红黑树的节点,从根节点开始,遍历比较当前链表节点与当前红黑树节点的
// hash值,dir用于保存比较结果,如果当前链表节点小,则dir为-1,否则为1,实际情况却是,能
// 拨到同一个桶位的所有元素的hash值那是一样的呀,所以dir的值是无法依靠hash值比较得出结果
// 的,那么重点就靠最后一个条件判断来得出结果了,
for (TreeNode p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);// 最后需要依靠这个方法来决定dir的值
TreeNode xp = p;
// 根据dir的值来决定将当前链表节点保存到当前树节点的左边还是右边,
// 或者当前链表节点需要与当前树节点的左节点还是右节点接着比较
// 主要寻找子节点为null的情况,将节点保存到null位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
// dir<=0,将链表节点保存到当前树节点的左边子节点位置
xp.left = x;
else
// dir<=0,将链表节点保存到当前树节点的右边子节点位置
xp.right = x;
// 一旦添加的一个新节点,就要进行树平衡操作,以此保证红黑树结构
// 树的平衡操作依靠的就是其左右旋转操作
root = balanceInsertion(root, x);
break;
}
}
}
}
// 最后将组装好的树的根节点保存到桶下标位
moveRootToFront(tab, root);
}
static void moveRootToFront(Node[] tab, TreeNode root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
// 首先定位桶下标位
int index = (n - 1) & root.hash;
TreeNode first = (TreeNode)tab[index];
// 校验当前桶下标位的值是否为根节点的值,可能会存在不同的原因是树的平衡操作将原本的根节点挪移了
// 如果相同,那么不作任何处理,如果不同,就需要替换桶位元素为树根节点元素,然后改变双向链表结构
// 将root根节点作为双向链表表头元素,为何要替换呢,因为在判断桶位元素类型时会对链表进行遍历,如
// 果桶位置放的不是链表头或者尾元素,遍历将变得非常麻烦
if (root != first) {
Node rn;
tab[index] = root;
TreeNode rp = root.prev;
if ((rn = root.next) != null)
((TreeNode)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
// 校验链表和树的结构
assert checkInvariants(root);
}
}
//...
}
//...
}
红黑树分拆操作
操作描述
很简单,看源码
源码解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
static final class TreeNode extends LinkedHashMap.Entry {
//...
// 将一颗大树分拆为两颗小树,如果树太小,退化为单向链表
final void split(HashMap map, Node[] tab, int index, int bit) {
// this代表当前节点,也就是树的根节点,桶位节点
// map代表当前集合
// tab代表新桶数组
// index代表当前节点的桶位下标
// bit为旧桶容量
TreeNode b = this;
// Relink into lo and hi lists, preserving order
TreeNode loHead = null, loTail = null;
TreeNode hiHead = null, hiTail = null;
int lc = 0, hc = 0;// lc表示低位树容量,hc表示高位树容量
for (TreeNode e = b, next; e != null; e = next) {
next = (TreeNode)e.next;
e.next = null;
// 分拆树节点的依据,结果为0的一组(低位组),结果不为0的一组(高位组)
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;
}
}
// 针对低位组进行树形化处理,如果该组元素数量少于6个则退化为单向链表
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
// 针对高位组进行树形化处理,如果该组元素少于6个则退化为单向链表
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
//...
}
//...
}
红黑树添加元素操作
操作描述
参照源码
源码解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
static final class TreeNode extends LinkedHashMap.Entry {
//...
// 红黑树的添加元素,map为当前HashMap,tab为当前桶数组,h为新增元素的key的hash值,k为新增元素的key,v为新增元素的value
final TreeNode putTreeVal(HashMap map, Node[] tab,
int h, K k, V v) {
Class> kc = null;
boolean searched = false;
// 当前节点是已定位的桶位元素,其实就是树结构的根节点元素
TreeNode root = (parent != null) ? root() : this;
for (TreeNode p = root;;) {
// dir代表当前树节点与待添加节点的hash比较结果
// ph代表当前树节点的hash值
// pk代表当前树节点的key
// 由于一个桶位的所有元素hash值相等,所以最后得出结果需要依靠
int dir, ph; K pk;
if ((ph = p.hash) > h)
// 如果当前节点的hash值大,dir为-1
dir = -1;
else if (ph < h)
// 如果当前节点的hash值小,dir为1
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
// hash值相等的情况下,如果key也一样直接返回当前节点,返回去之后会执行value的替换操作
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode 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))
// 这个找到的q也是与待添加元素key相同的元素,执行替换
return q;
}
// 最终需要依靠这个方法来得出dir值的结果
dir = tieBreakOrder(k, pk);
}
TreeNode xp = p;
// 根据dir的值来决定是当前节点的左侧还是右侧,如果该侧右子节点则继续循环寻找位置,否则直接将新元素添加到该侧子节点位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node xpn = xp.next;
TreeNode x = map.newTreeNode(h, k, v, xpn);//封装树节点
if (dir <= 0)
// dir<=0,将新节点添加到当前节点左侧
xp.left = x;
else
// 否则将新节点添加到当前节点右侧
xp.right = x;
// 设置新节点的链表位置,将其作为xp的下级节点
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
// 如果xp节点原本有下级节点xpn,则要将新节点插入到xp和xpn之间(指双向链表中)
((TreeNode)xpn).prev = x;
// 插入了新节点之后,要进行树平衡操作,平衡操作完成,将根节点设置为桶位节点
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
// 一般我们在HashMap中保存的键值对的类型都是不变的,这一般用泛型控制,
// 那么就意味着,两个元素的key的类型时一样的,所以才需要靠其hashCode来决定大小
// System.identityHashCode(parameter)是本地方法,用于获取和hashCode一样的结果,
// 这里的hashCode指的是默认的hashCode方法,与某些类重写的无关
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;
}
//...
}
//...
}
红黑树添加元素平衡操作
操作描述
左旋操作描述
绕A节点左旋,等于将A的右子节点B甩上来替换自己的位置,而自己顺势下沉成为其左子节点,这时你会发现,B有三个子节点,明显结构不对,将B的原来的左子节点C转移到下沉的A上,成为其右子节点,旋转结束
其实,要保证左子树节点值小于其根节点,右子树节点值大于其根节点,那么在替换AB节点之后,C节点的值就出现了问题,只有将其挪到A节点右边才能继续保证上面的结构。
首先我们知道B节点为A的右节点,那么B>A,而C为B的左节点,则CA,因此:A
右旋操作描述
绕A几点右旋,等于将A的左子节点B甩上来替换自己的位置,而自己顺势下沉成为其右子节点,这是你会发现,B有三个子节点,明显结构不对,将B的原来的右子节点C转移到下沉的A上,成为其左子节点,旋转结束 新增节点全部初始化为红色节点,然后分以下几种情况: 红黑树的节点删除操作主要分为这么三种: 针对第一种情况,真的好简单,待删节点即为叶子节点,直接删除即可; 针对第三种情况转换方法的解析:为什么要找到待删节点的右子树最左节点呢,因为红黑树是二叉搜索树,这个二叉搜索树中满足"左子节点<其父节点<其父节点的右子节点"的规则,那么找到的右子树的最左节点,就是整颗树中大于待删节点值的最小值节点了,为了保证二叉搜索树的搜索结构(也就是刚刚那个公式),我们只能找最接近待删节点值的节点值来接替它的位置,如此能保证二叉搜索的结构,但是可能会破坏红黑树的结构,因为如果待删节点为红色,而替换节点为黑色的话,那岂不是在待删节点分支多加了一个黑色节点嘛,还有其他各种情况,种种,需要进行删除节点后的树平衡操作来保证红黑树的结构完整。 下面重点说说删除后的平衡问题: 貌似有点难...大家要看进去思考才能理解,光看没用! HashMap中涉及到了数组操作,单向链表操作,双向链表操作,红黑树操作: 数组操作: 单向链表操作: 双向链表操作: 红黑树操作: 参考:
首先我们知道B为A的左子节点,所以BB,而C又位于A的左子树,则CC>B。要保证这个结果成立,那么再B替换A的位置之后,A下沉为B的右子节点,因为A>B,所以往右走,
这时C和A均位于B的右侧,比较二者发现C
添加平衡操作描述
源码解析
public class HashMap
红黑树删除元素操作
操作描述
针对第二种情况,也不难,将那个子节点替换待删节点即可;
至于第三种情况,那就麻烦了,但通过变换,可以将其转化为第一种或者第二种情况:处理方式是,找到待删节点的右子树中的最左节点(或者左子树中的最右节点),将其与待删节点互换位置,然后就将情况转换为第一种或者第二种了。
其实只要待删节点是黑色节点,一旦删除必然会导致分支中黑色节点缺一(红黑树不再平衡),具体情况又分为以下几种:(基础条件:待删节点p为黑色,其只有一个子节点x,操作在待删节点被删除之后,子节点替换其位置之后)
解析:开始情况是x分支删除了一个黑色节点,即x分支缺少一个黑色几点,而x的兄弟节点xpr为红色节点,xp为黑色节点,我们将xp和xpr颜色互换,那么在xpr分支黑色节点数量是不变的(只是位置变了),然后我么以红色的xp为基准执行左旋,将黑色的xpr甩上去替换xp的位置,xp作为xpr的左子节点,那么x端分支便多出了xpr这个黑色节点来补足不够的数量,而兄弟分支黑色节点数量还是不变的。
解析:新的sr(即原来的xpr)的原因是因为右旋操作之前执行了颜色替换,兄弟节点右侧分支少了一个黑色节点,右旋之后变为黑色的sl补充了这个黑色节点,但是现在我们要用sl[新xpr]来替换xp节点[置为xp节点的颜色],那么右侧分支原本用来补充之前缺少的黑色节点又消失了,所以将已知的红色节点sr置为黑色来进行补充)源码解析
public class HashMap
总结
【Java入门提高篇】Day25 史上最详细的HashMap红黑树解析
红黑树(一)之 原理和算法详细介绍