在1.8版本之前一直是数组加上链表的结构,1.8版本引入了红黑树,也就是用空间来换取时间,所以现在的结构就变成了数组+红黑树+链表
如下源码解释如果有问题的话,麻烦各位指正,谢谢。
在阅读本文章之前需要了解以下几个知识点
byte i_1 = 8; //00001000
byte i_2 = 31;//00011111
//=== 1. and运算 & === 相同位只要1个为0就是0
//00001000 即8
log.info("i_1 & i_2 {}",(i_2 & i_1));
//=== 2. or运算 | === 相同位只要一个为1即为1
//00011111 31
log.info("i_1 | i_2 {}",(i_2 | i_1));
//=== 3. xor异或运算 ^ === 相同位不同则为1,相同则为0。
//00010111 23
log.info("i_1 ^ i_2 {}",(i_2 ^ i_1));
//=== 4. not运算 ~ === 0和1全部取反 按位取反是对补码进行运算,当运算完后,再将补码变回原码。
// 00001000
//按位取反 11110111
// 减一 11110110
// 再取反 00001001 -9 符号位取反变为负 ~n = -(n+1)
log.info("~i_1 {}",(~i_1));
//=== 5. shl运算 << === a shl b就表示把a转为二进制后左移b位 末尾用0补充 高位遗弃
//i_1 << 2 00001000 向左移2位 00100000 即32
log.info("i_1 << 2 {}",(i_1 << 2 ));
//=== 5. shl运算 >> === a shl b就表示把a转为二进制后右移b位去掉末尾0
//i_1 >> 2 00001000 向右移2位 00000010 即2 最高位是0,左边补齐0;最高为是1,左边补齐1;
log.info("i_1 >> 2 {}",(i_1 >> 2 ));
//=== 6. 无符号位右移 >>> === 无符号右移,忽略符号位,空位都以0补齐
//i_1 >>> 2
链表由一个一个node节点组成,每一个节点都会指向下一个节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//每个node节点保存该节点key的hash值,利于后面的查找
final K key;
V value;
Node<K,V> next;//保存下一个节点的地址信息,该方法指向下一个节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
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; //红黑树特性,每个节点要么红色,要么其他色(黑色)
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* 获取根节点
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
/**
* 默认初始容量—必须是2的幂。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量,如果隐式指定较大的值时使用
*由任何一个带参数的构造函数。
*必须是2的幂<= 1<<30。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 构造函数中没有指定时使用的负载因子。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表节点转换红黑树节点的阈值, 9个节点转
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树节点转换链表节点的阈值, 6个节点转
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 转红黑树时, table的最小长度
*/
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,它的length总是2的幂次倍
transient Node<K,V>[] table;
// 存放具体元素的集
transient Set<Map.Entry<K,V>> entrySet;
// entrySet的个数也就是key-value的个数
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
/**
* Constructs an empty HashMap with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
//初始化负载因子
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs an empty HashMap with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty HashMap with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
//桶数量越界判断
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//桶数量最大不能超过MAXIMUM_CAPACITY 1<<30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//初始化负载因子0.75f
this.loadFactor = loadFactor;
//threshold 赋值大于initialCapacity且最近的2的整数次幂的数
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;
}
说白了就是如果n的二进制只有一个1的话就返回该数,否则就最左边的高位1进一位(其他位都为0)然后返回该数
构造函数总结
都会初始化负载因子,但是有参的情况下会计算参数的最接近2次幂的数,然后初始化桶的数量
/**
* 将指定值与此映射中的指定键关联。
*如果映射以前包含键的映射,则为旧的
值被替换。
*
* @param key 要与指定值关联的键
* @param value 值与指定的键关联
* @return the previous value associated with key, or
* null if there was no mapping for key.
* (A null return can also indicate that the map
* previously associated null with key.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash key的hash值
* @param key the key
* @param value the value to put
* @param onlyIfAbsent 为真时不会改变原有的值
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//p为(n - 1) & hash 对应索引位置的node头节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果数组没有被初始化则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//i = (n - 1) & hash 计算索引,如果该索引处为空值,就是没有放置node节点,就构建新的节点并放置在此处
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//如果该索引处有数据,证明存在node节点
Node<K,V> e; K k;
//如果存放进来的节点(后面统称为新节点)的hash值与该索引处节点的hash值,key值都相同的话,则将p赋值给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) //如果p为树节点,则以红黑树的方式去查找替换或者插入该节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//到这一步证明该节点为普通链表节点 遍历节点 binCount为节点个数
for (int binCount = 0; ; ++binCount) {
//如果p下一节点为空,证明该索引处只有一个node节点,即该节点为头节点
if ((e = p.next) == null) {
//对p的下一节点构建新的节点并设置该节点的下一节点为null
p.next = newNode(hash, key, value, null);
//binCount 大于等于满足转化红黑树阈值8 -1 的大小 减一是因为当前节点为p的下一节点 则调用treeifyBin方法将链表节点转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果hash值 key值相同 证明找到了目标节点 则退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不为空,证明新节点对应的索引处不为空存在node节点
if (e != null) { // existing mapping for key
V oldValue = e.value;
//如果onlyIfAbsent 为false 就是允许改变原有的值,或者oldValue 为空,则覆盖原有的值,并返回原有的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//结构修改次数自责你一
++modCount;
//如果插入节点后节点大于当前阈值的话 则调用resize 方法扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
*初始化或双精度表大小。如果为空,则分配in
*符合田间阈值设定的初始产能目标。
*否则,因为我们使用的是2的幂展开
*每个bin中的元素必须保持相同的索引,或者移动
*在新表中以2为偏移量的幂。
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//原来的数组大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//原来的阈值
int oldThr = threshold;
int newCap, newThr = 0;
//以下if的逻辑主要是是否扩展容量并重新对临界值进行相应的计算(负载因子乘以新数组的大小)
if (oldCap > 0) {
//数组最大数不能超过1<<30,否则返回最大数1<<30
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新数组大小较之前翻倍,翻倍的大小如果小于最大界限值并且原来的数组大小大于默认值16 则新的阈值较之前翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 初始容量设置为阈值
//这个时候一般hashMap都进行了初始化了
newCap = oldThr;
else { // 初始阈值为零表示使用默认值
// 新数组大小为默认值16
newCap = DEFAULT_INITIAL_CAPACITY;
//新的阈值为默认的负载因子乘以默认的数组大小
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//如果新的阈值为0则重新赋值,新的数组大小乘以负载因子,最大数为Integer.MAX_VALUE
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新的阈值赋值给临界值
threshold = newThr;
//创建newCap大小的Node数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//以下if语句主要是在原来的数组基础上,对新的newTab数组进行重新初始化操作
if (oldTab != null) {//如果原来的node数组含有数据
for (int j = 0; j < oldCap; ++j) {//遍历原来的node数组
Node<K,V> e;
if ((e = oldTab[j]) != null) {//获取下标为j的node节点赋值给e并判断空值
oldTab[j] = null; //将原来下标为j的节点置空
if (e.next == null)//如果e的下一个节点为空,证明原来数组该节点只有一个节点,所以可以直接通过key的hash值 对 新数组的小减一 与 运算得出该key对应的数组下标,然后将节点的值赋值到该位置
newTab[e.hash & (newCap - 1)] = e;
//如果该节点为红黑树,则以红黑树的方式进行维护节点
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//loHead 低位头节点 loTail 低位尾节点
Node<K,V> loHead = null, loTail = null;
//hiHead 高位头节点 hiTail 高位尾节点 注意所谓的低节点和高节点的区别就是高节点满足扩容后的条件即(e & (h *2 -1)),而低节点只满足(e & (h -1))无法满足挪移的条件
Node<K,V> hiHead = null, hiTail = null;
//next 下一个节点
Node<K,V> next;
do {
//获取到该节点的下一个节点
next = e.next;
if ((e.hash & oldCap) == 0) { //如果e的hash值与老表进行与运算为0,则扩容后的索引位置与老表保持一致
if (loTail == null) //如果低位尾节点loTail为空
loHead = e; //设置新的节点为低位头节点
else
loTail.next = e; //否则将新增的节点放到低位尾节点loTail的后面
loTail = e;//设置loTail 为新增的节点
}
else {//如果e的hash值与老表进行与运算不为0,扩展后的索引在原来的索引上加上原来数组的大小
if (hiTail == null)//如果高位尾节点为null
hiHead = e;//设置新的节点为高位头节点
else
hiTail.next = e; //否则将新的节点放到高位尾节点的后面
hiTail = e;//设置hiTail 为新增的节点
}
} while ((e = next) != null);
//如果低位尾节点loTail 不为空
if (loTail != null) {
//将低位尾节点loTail的后节点设置为空
loTail.next = null;
//将原数组索引的节点设置为低位头节点loHead
newTab[j] = loHead;
}
if (hiTail != null) {
//将高位尾节点hiTail的后节点设置为空
hiTail.next = null;
//在原数组索引的节点上加上原来的数组的大小上设置为高位头节点hiHead
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
现在我们以key Skindow作为参考来做个实验
Skindow 对应的哈希值为-482940273 二进制为 11100011 00110110 11101010 10001111
通过运算得出Skindow 对应的hash值为 -482997831
然后根据hash & (length - 1) 计算对应的下标
length 默认为16 即与15做与运算
这里有个巧妙的地方为什么减一,因为我在前面中的tableSizeFor方法提过,node数组的容量永远是2的次幂数,如果减一,那么导致1的右边全部为1,在做与运算,因为1的左边全部是0,所以与运算后这些位置就都是0,然后其他位置做与运算算出来的数(蓝色框处部分)就会小于等于数组的容量减一,而下标是又从零开始的,所以下标最大数为数组容量大小减去一。这里又跟上面进行了巧妙的结合。
那么为什么hash值为什么这样算呢?(h = key.hashCode()) ^ (h >>> 16)
首相h >>> 16 然后在与h做异或运算,通过上图我们可以得出结论让高位参与与低位的异或运算并且高位也被保留了下来,那么这样做的目的又是什么呢?
我个人的理解是,int类型本身是32位,用16位高位(就是自身的一半)去与低位做异或运算,总体来说说就是让高位参与进低位的索引运算,舍去了低位的精度,这样就避免了key精度较小的情况下会定位到同一个桶中的问题,所以这样做的好处是为了做好hash的均匀分布
回过头来再看看resize方法 用(e.hash & oldCap) == 0 来判断 新增的节点是放在原索引位置还是放在原索引位置加上原数组容量的大小的索引上的
我们先看看oldCap的特征,它为2的次幂数,那么将它与哈希值进行与运算,oldCap的为1的位置要么为0要么为1而其他位永远是0,所以进行与运算之后得出的结果要么是0要么是oldCap
所以这里将原来一个链表分成了两个链表,并分配到原来的索引位置上或者加上原来数组大小的索引位置上
但是我们回过头来想下,就是上图9下标位置上的新节点,算法为(16 -1) & hash值,与 原来的索引((8 -1) & hash) 加上8 按照设计原则两者应该是相等的
我们看下图 假设hash值为9
果然是相等的,那么我在来看看区别,当容量为8时,只有三位数参与有效运算当中(蓝色标记),当容量为16时,因为是翻倍的原因,所以参与有效运算的从三位增加到了四位,其中这四位中有三位是与之前一样的,这里注意,因为是一样的,所以这里其实可以不用运算,也就是直接将之前运算好的(8 -1) & hash拿来用,也就是如果四位是相同的,那么索引就是二进制1000加上(8 -1) & hash,这里正好和我们讨论的问题点对接上了。
(e.hash & oldCap) != 0 证明翻倍后的容量减一后的最左边的1和hash值对应的位置上数是相同的也是1否则就不是相同的,这种情况就满足将低位节点的元素往高位节点挪移,这样做的目的只有一个尽量保持node节点的均匀分布,减少hash冲突
上图忽略树节点
/**
* 将树仓中的节点分成上下两个树仓,
*如果现在太小,就不需要树。仅从resize调用;
*参见上面关于分割位和索引的讨论。
*
* @param map 该集合
* @param tab node数组
* @param index 当前节点在node数组的下标
* @param bit 旧数组的容量大小
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// 重新链接到lo和hi列表,保持顺序
//loHead 低位头节点 loTail 低位尾节点
TreeNode<K,V> loHead = null, loTail = null;
//hiHead 高位头节点 hiTail 高位尾节点 注意所谓的低节点和高节点的区别就是高节点满足扩容后的条件即(e & (h *2 -1)),而低节点只满足(e & (h -1))无法满足挪移的条件
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
//遍历node节点
for (TreeNode<K,V> e = b, next; e != null; e = next) {
//获取节点的下一个节点
next = (TreeNode<K,V>)e.next;
//将下一个节点置空
e.next = null;
//如果e的hash值与老表进行与运算为0 则扩容后的索引位置与老表保持一致
if ((e.hash & bit) == 0) {
//e.prev 上一个节点 如果lotail 为空 证明该节点为头节点
if ((e.prev = loTail) == null)
loHead = e;
else
//loTail不为空,则将将该节点e设置为loTail的下一个节点
loTail.next = e;
loTail = e;
//记录低位节点的个数
++lc;
}
else {// //如果e的hash值与老表进行与运算不为0 则扩容后的索引位置对应老表原先索引加上原先数组的容量大小
if ((e.prev = hiTail) == null)//e.prev 上一个节点 如果lotail 为空 证明该节点为头节点
hiHead = e;
else //loTail不为空,则将将该节点e设置为loTail的下一个节点
hiTail.next = e;
hiTail = e;
//记录高位节点的个数
++hc;
}
}
//如果低位头节点不为空
if (loHead != null) {
//如果低位节点的个数小于等于红黑树的阈值6
if (lc <= UNTREEIFY_THRESHOLD)
//loHead.untreeify按照链表结构拼接(非树节点),就是讲红黑树转化为普通链表结构,然后返回的节点赋值在原先的索引处
tab[index] = loHead.untreeify(map);
else {
//将低位头节点放置原先索引处
tab[index] = loHead;
//如果高位头节点不为空
if (hiHead != null) // hiHead 不为空,证明该红黑树被分成了两部分,所以需要重新对以loHead为根节点重新进行红黑树分布
//按照以loHead为根节点重新构建新的红黑树
loHead.treeify(tab);
}
}
//如果高位头节点不为空
if (hiHead != null) {
//如果高位节点的个数小于等于红黑树的阈值6
if (hc <= UNTREEIFY_THRESHOLD)
//将红黑树转化为链表结构,并放置在原先索引加上原数组容量大小处
tab[index + bit] = hiHead.untreeify(map);
else {
//将高位头节点放置在原先索引加上原数组容量大小处
tab[index + bit] = hiHead;
if (loHead != null)
//重新对以hiHead为根节点构建新的红黑树
hiHead.treeify(tab);
}
}
}
split 方法和 resize 方法里面的遍历node节点一块的逻辑相差不大,只不过split方法会判断节点的个数是否满足红黑树阈值,如果小于等于6 则将该节点转化为链表结构,否则重新构建新的红黑树
/**
*map 该hashMap实例队形
*tab hashMap里的node数组节点
*h 需存放key的hash值
*k 需存放的key
*v 需存放的value
* Tree version of putVal.
*/
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;
//当前节点的hash值大于需存放key的hash值
if ((ph = p.hash) > h)
dir = -1;
//当前节点的hash值小于需存放key的hash值
else if (ph < h)
dir = 1;
//当前节点的hash值等于需存放key的hash值 直接返回树节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果k没有实现Comparable接口,或者k和pk相同
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//搜索标识为false时
if (!searched) {
TreeNode<K,V> q, ch;
//将搜索标识改为true,searched第一次遍历后就设置为false,并且此时第一次的节点为根节点,也就是说默认第一次会根据判断hash值或者key值来决定左右遍历查找目标key
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;
}
//走到这里证明hash值相同且根据key值比较大小来遍布查询并没有找到目标对象,这时返回k和pk两者的系统hash值比较的大小结果,k小于等于pk返回-1否则返回1 给dir,相当于就是比较系统hash值来决定向左查询还是向右查询
dir = tieBreakOrder(k, pk);
}
//下一行代码p被赋值为当前p的左右节点,所以xp为p下面p的父节点
TreeNode<K,V> xp = p;
//比较系统hash值,hash值小于当前节点则向左边遍历查询否则向右边遍历查询,判断为空,是因为需要遍历二叉树找到左路劲还是右路劲下为空的情况,然后将要存入的k,v构建一个新的树节点存放到该出
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
//构建一个存放进来key,value的树节点,并且下一节点指向当前节点的下一个节点xpn,上一节点指向xp,且父节点也指向xp
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0) //如果系统hash值小于当前节点则向左边遍历
xp.left = x;
else //否则向右边遍历
xp.right = x;
//xp下一节点指向x节点
xp.next = x;
//x的上一节点指向xp,且父节点也指向xp
x.parent = x.prev = xp;
//如果当前 x 节点下一节点不为空,则将下一节点的上一节点设置为当前节点x
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//红黑树插入平衡调整
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
/**
* Returns x's Class if it is of the form "class C implements
* Comparable", else null.
*/
static Class<?> comparableClassFor(Object x) {
//如果对象x实现了Comparable
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
//如果对象x的实体类对应为String类则直接返回对象x的实体类
if ((c = x.getClass()) == String.class) // bypass checks
return c;
//调用getGenericInterfaces获取实体类c的所有实现类接口里面的type或者类并遍历这些类和type,Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。
if ((ts = c.getGenericInterfaces()) != null) {
for (int i = 0; i < ts.length; ++i) {
//ParameterizedType结合getRawType()获取这些实现接口的参数化类型,并且和Comparable相等
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
//获取参数化类型里面的参数,并且参数类型只有一个当且类型为c,因为Comparable 只有一个参数泛型,这里是对Comparable的一个校验
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
该方法主要检查对象是否实现了Comparable接口,如果没有返回空,否则返回运行类
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
*/
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;
//如果当前节点hash值小于存放的hash值则向树节点的左边遍历
if ((ph = p.hash) > h)
p = pl;
//如果当前节点hash值大于存放的hash值则向树节点的左边遍历
else if (ph < h)
p = pr;
//如果当前节点key值等于存放的key值则直接返回该节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果该节点下没有左节点则遍历右边的节点
else if (pl == null)
p = pr;
//如果该节点下没有右节点则遍历左边的节点
else if (pr == null)
p = pl;
//如果k实现了Comparable接口,并且存放的k值小于当前节点的k值则遍历做节点,否则遍历右节点
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
//到这一步证明k实体类没有实现Comparable接口,向右边遍历
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
//到这一步,证明刚刚pr.find 右边节点遍历没有找到对应的目标key,这时向左边遍历
p = pl;
} while (p != null);
return null;
}
通过比较hash值得大小,如果存放的hash值大于当前hash值,则往右边遍历,否则向左边遍历,如果当前节点没有左节点则往右边遍历,没有右节点就左边遍历,如果存放的节点key实现了Comparable接口,则利用比较key值来遍历做左右节点,小就遍历做节点,大就遍历右节点。原则是左小右大
/**
* Returns root of tree containing this node.
*/
//循环遍历父节点,如果父节点为null则证明该节点为根节点,并返回
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* Tie-breaking utility for ordering insertions when equal
* hashCodes and non-comparable. We don't require a total
* order, just a consistent insertion rule to maintain
* equivalence across rebalancings. Tie-breaking further than
* necessary simplifies testing a bit.
*/
static int tieBreakOrder(Object a, Object b) {
int d;
//a对象和b对象实体类的类名相同,则判断两者的系统hash值(根据两者的内存地址得出),a小于等于b的系统hash值返回-1 否则返回1
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;
}
/**
* 确保给定根是其bin的第一个节点。
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
//如果root节点不为空,且node数组已经被初始化过
if (root != null && tab != null && (n = tab.length) > 0) {
//获取root节点在node数组处的索引
int index = (n - 1) & root.hash;
//通过Index索引获取到对应的头节点first
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
//如果头节点和root节点不相等
if (root != first) {
Node<K,V> rn;
//用根节点覆盖原有的头节点
tab[index] = root;
//获取root节点的上一节点rp
TreeNode<K,V> rp = root.prev;
//如果root的下一节点不为空,则root的上一节点为root的下一节点的上节点,
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
//如果rp不为空,则root的下一节点为root的上一节点的下一节点
if (rp != null)
rp.next = rn; //上面两个步骤相当于移除了root的角色,将root的上一节点和root的下一节点直接绕过root互相匹配
//如果头节点不为空,则root节点为头节点的上一节点
if (first != null)
first.prev = root;
//root的下一节点为first
root.next = first;
//将root的上一节点置null
root.prev = null;
}
//红黑树插入检查,不开启某种debug参数,该行代码不会运行,不影响整体流程
assert checkInvariants(root);
}
}
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 {
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);
}
}
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)//将p的右节点的左节点作为p的右节点并互相指向子父节点
rl.parent = p;
if ((pp = r.parent = p.parent) == null)//将p的右节点作为p父节点的右节点,相当于替换两者的位置并互相指向子父节点
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
//将p作为p右节点的左节点并互相指向子父节点
r.left = p;
p.parent = r;
}
return root;
}
初始状态(pr 为 r)
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
支点与其右节点互换位置,右节点继承支点的父节点属性,而支点则继承右节点的左节点属性,之后支点连接右节点,支点成为右节点的左节点,而右节点成为支点的父节点。
(5.13 以下论点有待商榷,有问题)
我们发现:在左旋之前路径皆为4个,在左旋之后就变成了3和4,可以得出结论,无论是左旋还是右旋都是在合理左旋右旋的前提下,这样做的目的是为了缩小树各个的树路径的平衡差,保持平衡二叉树的特性,左旋和右旋的前提条件是比如左节点必须大于父节点,父节点小于右节点。仔细看上图发现,在左旋之前,p
由此得出,左旋的触发条件为一个节点(p)小于右节点的左节点会发生左旋,这时p的右节点会成为它的父节点,原有的右节点的左节点会成为p的右节点
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
初始状态(pl为l)
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
前后对比图
支点与其左节点互换位置,左节点继承支点的父节点属性,而支点则继承左节点的右节点属性,之后支点连接左节点,支点成为左节点的右节点,而左节点成为支点的父节点。
(5.13 以下论点有待商榷,有问题)
左图我们发现,p大于pl,plr也大于pl,但是无法判断p是否大于plr,但是右图我们发现p是大于plr的。
由此得出右旋的触发条件,当一个支点它大于它的左节点的右节点时会发生右旋,它的左节点成为它的父节点并且它成为该父节点的右节点,原有的左节点的右节点会成为它的左节点
通过左旋和右旋我们不难发现,一般发生左旋和右旋之前,都存在右两个数存在不合理的情况,比如无法得出某两个数谁大谁小,再通过左旋右旋之后就可以直观的得出任意两个数的大小。我们可以得出出一个原则就是平衡二叉树不存在任意两数存在模糊不清的关系,一旦有就要通过左旋和右旋来优化这个问题。
注意看下方代码需要了解红黑树的基本特性,以及树的左旋右旋上面都有介绍
红黑树的插入流程图如下
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没有父节点,说明x节点即根节点,按照红黑树特性,设置当前节点为黑节点(非红即黑),并返回当前节点x(根节点)
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果当前x节点父节点为黑节点,或者x节点父节点的父节点为空,换句话说就是这里判断x节点处于是否处于第二阶级,这些情况都不需要重新调整 返回根节点root
else if (!xp.red || (xpp = xp.parent) == null)
return root;
if (xp == (xppl = xpp.left)) {//走到这里证明x节点不是根节点,且x节点位于第二层的下面 如果x的父节点在x增父节点的左边
if ((xppr = xpp.right) != null && xppr.red) {//如果x的父节点在x增父节点的左边且曾父节点的右节点为红色节点 这是就出现了x节点和xp节点xppr都为红色节点,为了保持红黑树的特性需要变色,上一层级变为红色,上上层变为黑色。
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {//如果x的父节点在x增父节点的左边 且x的曾祖父右节点要么为空要么为黑色节点
if (x == xp.right) { //如果这时候x的父节点右节点为x 则以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);
}
}
}
}
}
}
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
*
A return value of {@code null} does not necessarily
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
5.1 这里主要调用了getNode方法,下面我们看看这个方法
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
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) {
//索引处的头节点hash值,key值与存放进来的hash值,key值都相同,则直接返回该节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果索引处的头节点含有下一个节点不为空
if ((e = first.next) != null) {
//如果当前节点为树节点,则以红黑树的查找方式查找key,并返回对应的节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//遍历索引处的所有节点,直到查找到的node节点hash值和key值都相同或者没有下一个节点为止并返回该节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//没有找到返回null
return null;
}