JDK1.8
通过put中的key去取到下标。key---->key.hashcode()----->然后去取余table.lenght------>inde-----0-7
hashCoude转换取余以后会转换成数组的下标比如1,2等。
用数组去存储的数据,指定下标去查找,时间复杂度O(1),对于一般的插入操作,涉及到数组元素的移动,平均复杂度是O(N).
数组长度16为什么要去减去1,然后进行与运算。
**
重点
链表转化为红黑树,需要链表的长度大于8,但是在转变成R-B tree之前,还会有一次判断,只有键值对数量大于 64 才会发生转换。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
//无参构造方法:
public HashMap() {
//给hash赋值负载因子:
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
/**
* The load factor used when none specified in constructor.
*/
//hash因子是0.75:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//有参构造方法 指定一个初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
//判断传入的初始容量如果小于零就抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始化大小的,如果传入的容量大于int最大值的一个2的指数幂,那么传入的容量就为int最大值的2的指数次幂.
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果负载因子小于0或者不等就抛出异常.
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//赋值
this.loadFactor = loadFactor;
//见下方详细:
this.threshold = tableSizeFor(initialCapacity);
}
/*
1.HashMap在确定数组下标Index的时候,采用的是( length-1) & hash的方式,只有当length为2的指数幂的时候才能较均匀的分布元素。
z所以HashMap规定了其容量必须是2的n次方
2.由于HashMap规定了其容量是2的n次方,所以我们采用位运算<<来控制HashMap的大小。使用位运算同时还提高了Java的处理速度。
HashMap内部由Entry[]数组构成,Java的数组下标是由Int表示的。所以对于HashMap来说其最大的容量应该是不超过int最大值的一个2的指数幂,
而最接近int最大值的2个指数幂用位运算符表示就是 1 << 30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* Returns a power of two size for the given target capacity.
*/
//对于给定的目标容量,返回两倍大小.
//传入11那么就是16,传入33那么就是64,这里指定的数必须是2的指数次幂.
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;
}
//|
例子:
11 二进制数为:0000 1011
11>>>1 ---------:0000 0101
| ---------:0000 1111
4 0000 0100
n>>>1 0000 0010
n| 0000 0110
n>>>2 0000 0011
n| 0000 0111
n>>>4 0000 0000
n| 0000 0111
n>>>8 0000 0000
n| 0000 0111
n>>>16 0000 0000
n| 0000 0111
n=7
(7 < 0) ? 1 : (7 >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : 7 + 1;
1.1putVal():
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//声明Node数组tab,Node节点P,int n,i。
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判读table的长度是否为空,且把值赋给tab。tab的长度是否等于0且赋值给n。
if ((tab = table) == null || (n = tab.length) == 0)
//进来说明table等于null且长度等于0,
//进入resize扩容方法,扩容后把tab的长度赋值给n。
n = (tab = resize()).length;
//判断传入进来的值所在的数组坐标下的第一个位置是否为空
//1.1.1:i=(n-1)&hash计算数组下标位置且把值赋给i。tab[i]得到数组下标位置的值。把值赋
//给P。
if ((p = tab[i = (n - 1) & hash]) == null)
//进来说明得到的数组下标位置中的值为空。
//newNode:创建新的Node节点传值放到tab[i]:得到的数组下标位置中。且指向下一节点的node
//为null
tab[i] = newNode(hash, key, value, null);
else {
//进来这里说明,数组不为空,且得到的数组下标位置中的值也不为空。
Node<K,V> e; K k;
//p在上面1.1.1已经被赋值。
//判断数组下标中的值的hash是否等于传入进来的参数hash值相等,key不为空且相等。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//进来说明相等,那么就直接把P的值赋给e。下面1.1.2中替换掉value值。
e = p;
//判断p是否属于类型TreeNode。
else if (p instanceof TreeNode)
//进来这里说明是TreeNode类型(红黑树),进行putTreeVal方法中。此方法在1.3中讲解
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//进来这里说明数组不为空,得到的数组下标位置中的值也不为空。数组下标的hash值和传入的
//hahs值不想等,数组下标中的值不是TreeNode类型。
/*这里循环链表。第一:判断链表的长度是否超过8,链表超过8进入treeifyBin方法(),这个
方法里面还有判断。第二:传入的参数是否和链表中的hash和key值相等。
JDK1.8是尾插法*/
for (int binCount = 0; ; ++binCount) {
//判断链表p的下一个节点是否为空且赋值给e。
if ((e = p.next) == null) {
//进来这里说明是链表最后一个节点了。最后一个节点的next为null。
//把传入进来的参数newNode节点指向p的next下一个节点中
p.next = newNode(hash, key, value, null);
/*判断循环的次数是否大于等于8-1,数组下标是0开始算,所以判断链表是否
大于8,链表中的数量其实是9.在p.next = newNode(hash, key, value,
null);赋值了,但是没有++binCount。*/
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//如果大于等于7既然怒treeifyBin方法中。
treeifyBin(tab, hash);
//跳出循环
break;
}
//这里判断循环的某个节点的hash值和key是否相等于传入进来的hash和key值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//如果是:跳出循环
break;
//这里是在循环的时候把(e = p.next),当前节点的下一个节点赋值给p。循环下
//一个节点
p = e;
}
}
//1.1.2:判断e!=null说明找到了链表中hash和key等于传入进来hash和key值相等的情况。
if (e != null) { // existing mapping for key
//e节点的value赋值给oldValue。e节点本身的value值。
V oldValue = e.value;
//判断onlyIfAbsent为true或者oldValue为null才进入。
if (!onlyIfAbsent || oldValue == null)
//把传入的value赋值给e.value,e节点的hash,key是和传入值的hash和key相等
//的。
e.value = value;
afterNodeAccess(e);
//返回e节点本身的oldValue,返回的并不是传入的value值。
return oldValue;
}
}
++modCount;
//每次都会走这一步,判断元素加入的次数是否大于阈值,如果大于就会扩容。
if (++size > threshold)
//扩容方法
resize();
afterNodeInsertion(evict);
//返回null。
return null;
}
1.2 resize方法()
//扩容方法
final Node<K,V>[] resize() {
//把table的数组赋值给oldTab
Node<K,V>[] oldTab = table;
//三元运算符:oldTab == null第一次table是null那么oldCap=0
//lodTab!=null那么oldCap = 数组的长度。
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr=阈值,第一次进入threshold为0。
int oldThr = threshold;
int newCap, newThr = 0;
//判断如果数组的长度是否大于0,说明判断是否第一次进入扩容。
if (oldCap > 0) {
//static final int MAXIMUM_CAPACITY = 1 << 30;
//判断oldCap是否大于MAXIMUM_CAPACITY作2的幂方中最大值
/*
threshold,如果对hashmap有一点了解的人都会知道threshold = 初始容量 * 加载因子。
也就是扩容的 门槛。相当于实际使用的容量。而扩容都是翻倍的扩容。那么当容量到达
MAXIMUM_CAPACITY,这时候再扩容就是 1 << 31 整型溢出。所以Integer.MAX_VALUE作为最终
的容量,但是是一个threshold的身份。
*/
if (oldCap >= MAXIMUM_CAPACITY) {
//阈值等于
//他达不到2^31,因为Integer的最大值就是2^31-1,如果threadhold超过2^30,会把
//Integer的最大值赋给他
threshold = Integer.MAX_VALUE;
return oldTab;
}
/*static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
否则,判断oldCap数组的长度左移一位,相当于数组长度乘以2且赋值给新newCap后是否小
于2的幂方中最大值且数组长度是否大于16。
第一中情况,数组长度乘以2且赋值给新newCap后小于2的幂方中最大值,但数组长度小于
16,跳出循环
第二种情况,数组长度乘以2且赋值给新newCap后于2的幂方中最大值且数组长度大于16。
第三种情况,数组长度乘以2且赋值给新newCap后大于2的幂方中最大值。
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//进入这里说明:数组长度小于2的幂次方最大值且数组长度大于16
//数组长度二进制数向左移动一位,也就是数组长度乘以2,赋值给新newThr
newThr = oldThr << 1; // double threshold
}
//如果数组长度不大于0,阈值oldThr大于0,当在new HashMap的时候指定了数组长度和hash因子。
else if (oldThr > 0) // initial capacity was placed in threshold
//阈值赋值给newCap
newCap = oldThr;
//如果数组长度不大于0,阈值oldThr小于0
else { // zero initial threshold signifies using defaults
//newCap = 16,newThr=0.75*16,第一次进入扩容的方法时。
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//判断newThr是否等于0,
if (newThr == 0) {
//情况一:数组的长度大于0,小于16。进入
//情况二:oldThr=threshold>0,阈值>0的时候。进入
//ft = newCap新数组长度乘以hash因子0.75
float ft = (float)newCap * loadFactor;
//newThr = 新数组长度小于2的幂方中最大值且新数组乘以0.75的值小于2的幂方中最大值,
//就是ft,否则为Integer.MAX_VALUE。
//这里的三元运算是为了第二种情况。在1.6中构造方法,指定的数组长度是2的幂方中最大值
//赋给了阈值。如果newCap为false那么newThr新阈值为Integer.MAX_VALUE=0x7fffffff
//Integer的最大值就是2^31-1
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//把新的阈值赋给threshold阈值。
threshold = newThr;
//创建新的数组。长度扩大两倍。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//判断旧的数组不为null
if (oldTab != null) {
//循环遍历旧数组的长度
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//判断旧数组下标中的Node节点是否为null,不为空进入。
if ((e = oldTab[j]) != null) {
//首先让旧数组下标中的值赋值为null
oldTab[j] = null;
//判断节点的下一个node节点是否为null
if (e.next == null)
//如果为空,那么就把旧数组下标中的节点.hash&(新数组长度-1)得到新数组下标
//的位置赋值上e节点。
newTab[e.hash & (newCap - 1)] = e;
//判断节点是否是红黑树
else if (e instanceof TreeNode)
//是红黑树split详解与1.3
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
/*
loTail指向当前的元素e
初始化后,loTail和loHead指向相同的内存,所以当loTail.
next指向下一个元素时,底层数组中的元素的next引用也相应发生变化,
造成lowHead.next.next.....跟随loTail同步,使得lowHead
可以链接到所有属于该链表的元素。
在这里进行低位,高位,让数组下标中的链表更短,查询速度也就提高,
可以避免死环的问题。
*/
// 存储索引位置为:“原索引位置”的节点
Node<K,V> loHead = null, loTail = null;
// 存储索引位置为:“原索引位置+oldCap”的节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/*如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置
跟老表的索引位置一样。这里把旧数组迁移到新数组中和JDK1.7中的迁移数据
区别。具体JDK1.7可以看(一)中详解。
*/
if ((e.hash & oldCap) == 0) {
//这里是低位,判断地位中loTail节点是否为null
if (loTail == null)
//把此节点赋值给地位的头loHead,位置不会变化。
loHead = e;
else
//第二次进入后,会把上一个与运算为0的节点指定当前节点。
loTail.next = e;
//把当前节点赋值给loTail,用于下次再进入时,.next指向传入进来的
//节点形成链表
loTail = e;
}
else {
//高位判断e.hash & oldCap!=0会进入,那么旧数组中与运算不等于0的
//节点形成的链表,会在新数组中旧数组下标的一倍的位置上
if (hiTail == null)
//把当前节点赋值给hihead,高位的头节点
hiHead = e;
else
//把高位的next指向当前节点。
hiTail.next = e;
//把当前节点赋值给hiTail,于下次再进入时,.next指向传入进来的
//节点形成链表
hiTail = e;
}
//把next赋值给了e,下一个节点不会空,那就继续循环下一个节点。
//循环到e的next==null为止。也就是最后一个节点。
} while ((e = next) != null);
//判断低位的loTail不等于null,就是有低位有值。
if (loTail != null) {
//让低位的next = null,因为可能低位的next在与运算的时候不等于0进入
//高位链表中了。直接赋为null
loTail.next = null;
//让新的数组j的位置等于低位。
newTab[j] = loHead;
}
//判断高位的hiTail不等于空,高位又是否有值。
if (hiTail != null) {
//让高位的next=null,因为可能高位的next在与运算的时候等于0进入
//低位链表中了。
hiTail.next = null;
//让新数组的j下标位置加上数组下标长度位置。
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
/**
* 测试目的:理解HashMap发生resize扩容的时候对于链表的优化处理:
* 初始化一个长度为8的HashMap,因此threshold为6,所以当添加第7个数据的时候会发生扩容;
* Map的Key为Integer,因为整数型的hash等于自身;
* 由于hashMap是根据hash &(n - 1)来确定key所在的数组下标位置的,因此根据公式 m(m >= 1)* capacity + hash碰撞的数组索引下标index,可以拿到一组发生hash碰撞的数据;
* 例如本例子capacity = 8, index = 7,数据为:15,23,31,39,47,55,63;
* 有兴趣的读者,可以自己动手过后选择一组不同的数据样本进行测试。
* 根据hash &(n - 1), n = 8 二进制1000 扩容后 n = 16 二进制10000, 当8的时候由后3位决定位置,16由后4位。
*
* n - 1 : 0111 & index resize--> 1111 & index
* 15 : 1111 = 0111 resize--> 1111 = 1111
* 23 : 10111 = 0111 resize--> 10111 = 0111
* 31 : 11111 = 0111 resize--> 11111 = 1111
* 39 : 100111 = 0111 resize--> 100111 = 0111
* 47 : 101111 = 0111 resize--> 101111 = 1111
* 55 : 110111 = 0111 resize--> 110111 = 0111
* 63 : 111111 = 0111 resize--> 111111 = 1111
*
* 按照传统的方式扩容的话那么需要去遍历链表,然后跟put的时候一样对比key,==,equals,最后再放入新的索引位置;
* 但是从上面数据可以发现原先所有的数据都落在了7的位置上,当发生扩容时候只有15,31,47,63需要移动(index发生了变化),其他的不需要移动;
* 那么如何区分哪些需要移动,哪些不需要移动呢?
* 通过key的hash值直接对old capacity进行按位与&操作如果结果等于0,那么不需要移动反之需要进行移动并且移动的位置等于old capacity + 当前index。
*
* hash & old capacity(8)
* n : 1000 & index
* 15 : 1111 = 1000
* 23 : 10111 = 0000
* 31 : 11111 = 1000
* 39 : 100111 = 0000
* 47 : 101111 = 1000
* 55 : 110111 = 0000
* 63 : 111111 = 1000
*
* 从下面截图可以看到通过源码中的处理方式可以拿到两个链表,需要移动的链表15->31->47->63,不需要移动的链表23->39->55;
* 因此扩容的时候只需要把loHead放到原来的下标索引j(本例j=7),hiHead放到oldCap + j(本例为8 + 7 = 15)
*
* @param args
*/
public static void main(String[] args) {
HashMap<Integer, Integer> map = new HashMap<>(8);
for (int i = 1; i <= 7; i++) {
int sevenSlot = i * 8 + 7;
map.put(sevenSlot, sevenSlot);
}
}
1.3 split()方法
/** 这个方法在HashMap进行扩容时会调用到: ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
* @param map 代表要扩容的HashMap
* @param tab 代表新创建的数组,用来存放旧数组迁移的数据
* @param index 代表旧数组的索引
* @param bit 代表旧数组的长度,需要配合使用来做按位与运算
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
//把当前节点赋给b
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
/*
这里在1.2中讲过.其实这里也是把当前数组下标中为为红黑树结构的节点.通过(e.hash & bit) == 0
方式,再次的进行分为高位和地位.并且计算地位的++lc和高位的++hc,判断是否长度是否小于6,也就是说
如果小于6,会转换成链表.如果大于6且高(低)位不为空,就会重新转换成红黑树,因为通过高低位的放值后,
节点中的红黑树已经被打乱了,可以说不是一棵树了,需要重新排列组成红黑树并且放到扩容后新数组的
高(低)位的下标中.
在扩容后,因为数组长度变长,分成高低位这样的做可以使得链表变短或红黑树节点变少,从而使得查询效率
提高
*/
// 存储索引位置为:“原索引位置”的节点
TreeNode<K,V> loHead = null, loTail = null;
// 存储索引位置为:“原索引位置+oldCap”的节点
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
//循环遍历当前下标中红黑树的节点.
for (TreeNode<K,V> e = b, next; e != null; e = next) {
//把当前节点的下一个节点赋值给next
next = (TreeNode<K,V>)e.next;
//取好e的下一节点后,把它赋值为空,方便GC回收,也是在如果最后一个e节点让他的next指向空
//放到连表的最后,连表最后一个节点的next是为null的.
e.next = null;
if ((e.hash & bit) == 0) {
//判断第一次进入的时候loTail为null赋值给loHead,后面判断使用.
//e.prev = loTail:是把当前节点的prev指向了上一个节点
if ((e.prev = loTail) == null)
loHead = e;
else
//上一个节点的next指向当前节点
loTail.next = e;
//把上一个节点的loTail换成当前节点,那么上一个节点已经存放好了,当前节点便于下次
//循环指向.
loTail = e;
//累计加上放出的次数,用于判断是进行树化
++lc;
}
else {
//高位数组下标存放的节点:
//判断和上面一样
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
//进行高位的放入节点次数的累计加,用于判断是进行树化
++hc;
}
}
//判断低位是否为null.
if (loHead != null) {
//判断低位累加次数是否小于等于6,如果是小于,就进行链表化,否则进行下一步判断.
//static final int UNTREEIFY_THRESHOLD = 6;
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
//这里如果高位不为null,那么就把链表放到当前数组下标中
//这里如果高位为null,那么就把红黑树放到当前数组下标中.因为近来split方法中时,
//已经判断是否是红黑树了,既然高位没有值,那么所有的节点都还是组成的一棵树没有分散.
tab[index] = loHead;
//判断高位中节点是否为null,不为null就进行树化,为null就继续往下走.
if (hiHead != null) // (else is already treeified)
//进行树化的方法在1.5中有详细介绍
loHead.treeify(tab);
}
}
//判断高位是否为null.
if (hiHead != null) {
//判断高位累加次数是否小于等于6,如果是小于,就进行链表化,否则进行下一步判断.
//static final int UNTREEIFY_THRESHOLD = 6;
if (hc <= UNTREEIFY_THRESHOLD)
//index+bit这里就是加了原来位置的一倍的位置,高位在这里.
//跟上面一样
tab[index + bit] = hiHead.untreeify(map);//untreeifyf()方法在1.4中
else {
//这里和上面一样的逻辑
tab[index + bit] = hiHead;
//判断低位中节点是否为null,不为null就进行树化,为null就继续往下走.
if (loHead != null)
//进行树化的方法在1.5中有详细介绍
hiHead.treeify(tab);
}
}
}
1.4:untreeify()方法
final Node<K,V> untreeify(HashMap<K,V> map) {
//声明内存地址相同的hd和tl
Node<K,V> hd = null, tl = null;
//Node<K,V> q = this:指的当前红黑树节点。
for (Node<K,V> q = this; q != null; q = q.next) {
//把TreeNode<K,V> q 转成 Node<K,V> p
Node<K,V> p = map.replacementNode(q, null);
//第一次tl为null,hd = p
if (tl == null)
hd = p;
else
//第二次进入的时候,第一个节点的next指向了p
tl.next = p;
//tl和hd内存地址相同指向p。
//第二次进入的时候,把第二个节点赋值给了tl,此时tl和上一个节点也就窜起来了。
tl = p;
}
//因为内存地址相同,指向也就一样。返回链表。
return hd;
}
1.5 treeify()树化的方法
final void treeify(Node<K,V>[] tab) {
//声明一个root的红黑树节点根节点
TreeNode<K,V> root = null;
//这里进行循环这个链表.从新组成红黑树.TreeNode<K,V> x = this, next.声明并赋值当前节点.
//此for循环会一直循环到next ==null为止,组成一棵红黑树.
for (TreeNode<K,V> x = this, next; x != null; x = next) {
//当前循环节点的next也就是当前循环节点的下一个节点赋值给next.
next = (TreeNode<K,V>)x.next;
//把当前节点的作叶子和又叶子赋值为null
x.left = x.right = null;
//判断root是否等于null,这里直接把第一个值放到根节点.当第二次进入的时候root不会为null
if (root == null) {
//把当前节点父指向设为null,他是root根节点没有父指向
x.parent = null;
//把颜色设置为黑色
x.red = false;
//root等于x,根节点赋值.
root = x;
}
//第二次进入root不会为null,进入这里.
else {
//什么k为当前节点的key值
K k = x.key;
//h为当前节点的hash值
int h = x.hash;
//声明kc为null.
Class<?> kc = null;
//这里又是一个for循环.for循环中的for循环,此for循环是寻找某个节点是属于这个棵数的
//中某个叶子节点的左节点还是右节点,会一直寻找,外围的for循环呢,是循环链表直到next为
//null,也就是把整个链表全部组成一棵树.两个for循环的功能不同.
for (TreeNode<K,V> p = root;;) {
//这里声明了dir,ph这里是上一个节点所放值的参数.
int dir, ph;
//pk等于p.key 上一个节点的key
K pk = p.key;
//判断上一个节点的key是否大于h,并且把上一个节点的hash值赋值给了ph.
if ((ph = p.hash) > h)
//如果当前节点的hash值h小于上一个节点的hash值ph,那么当前节点应该在上一个节点的
//左子树上
dir = -1;
//否则当前节点hash值大于上一个节点的hash值,那么当前节点在上一个节点的右子树上
else if (ph < h)
dir = 1;
//如果既不大于也不小于的话,会进入一下方法,最终得出dir的值.
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
//这里把上一个节点p赋值给了xp
TreeNode<K,V> xp = p;
//这里先判断dir是否小于0(左子树)还是大于0(右子树),等于null的情况下进入方法中.
//这里(p = (dir <= 0) ? p.left : p.right)这里给p赋值了,如果不为null,那么
//当前节点所在上一个节点的(左)右不为null,那么就把上一个节点的左(右)节点赋值给
//p,这里就是里面for循环继续寻找,直到找到为null的左(右)节点为止.
//for (TreeNode<K,V> p = root;;)
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//如果dir小于0(左子树)还是大于0(右子树)的(左)右节点为null了进入这里.
//把当前节点的父节点指向上一个节点xp,xp在上面等于了p,而p就是上一个节点.
x.parent = xp;
//如果dir小于0,那么就在上一个节点的左叶子树上.
if (dir <= 0)
//上一个节点的left(左节点)等于当前节点
xp.left = x;
else
//上一个节点的right(右节点)等于当前节点
xp.right = x;
//进入此方法中传入root和当前节点,进行红黑树化.返回的是root的节点,这里是用
//外层for循环,循环到链表next==null为止
//参数为当前root和当前的节点x.x是已经指向好了的父类,父类的左(右)节点了已经
//确定好了.
root = balanceInsertion(root, x);//此方法1.6详解.
//这里跳出当前内循环.继续外部的循环.
break;
}
}
}
}
//此方法用于把调整根节点到当前坐标下的位置中。具体细节看标题1.7
moveRootToFront(tab, root);
}
**
1.6、balanceInsertion()方法
**
//root 红黑树
//x 是当前节点
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
//首先让x为红色
x.red = true;
//for循环,声明统一内存地址xp:父节点,xpp:祖父节点,xppl:祖父节点的左节点,xppr:祖父节点的又节点
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//x的父节节点赋值给xp 判断是否等于null,如果为null,那么当前节点会变成黑色.返回x.
//在treeify中已经有判断if(root==null),说明父节点就不会为null,这里判断 当前节点x的父节点为null,
//因为:这里是for循环红黑树,调整整个红黑树的情况,会遍历到最后的节点判断节点是什么颜色,最终形成一颗完整的红黑树.
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//xpp = xp.parent把父节点的父节点赋值给祖父节点
//否则判断父节点如果为黑色 或者 祖父节点为null,那么就直接返回root了.
//因为:一开始进入此方法当前节点就赋值为红色,x.red,那么父节点现在为黑色(false),!xp.red=true,直接就是红黑树返回.
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//以上两种判断都不成立进入一下方法,父节点不为null,父节点不为黑色或者祖父节点不为null.
//祖父节点的左节点赋值给xppl
//判断父节点是否是祖父节点的左节点,是:进入下面方法,不是进入else方法.
if (xp == (xppl = xpp.left)) {
//xppr = xpp.right 祖父节点的右节点赋值给xppr
//判断祖父节点的右节点不等于null-且-祖父节点的右节点为红色.进入下面方法.
//这里的判断需要了解红黑树的原理.
if ((xppr = xpp.right) != null && xppr.red) {
//祖父节点的右节点设置为黑色
xppr.red = false;
//父节点的颜色设置为黑色
xp.red = false;
//祖父节点设置为红色
xpp.red = true;
//一开始进入的当前节点x.red = true;
//下面插入图片1.1.
/*
祖父节点为红色
祖父节点右节点为黑色
祖父节点的左节点为黑色
当前节点为红色
*/
//祖父节点赋值给当前节点,这里拿到for循环中继续循环这棵树,这里已经做好了三个层级节点.
//if ((xp = x.parent) == null) {x.red = false;return x;}这里继续循环会判断x(xpp)的父节点为null,那么x(xpp)就
//会为黑色
//最终形成
/*下面插入图片1.2
祖父节点为黑色
祖父节点右节点为黑色
祖父节点的左节点为黑色
当前节点为红色
*/
x = xpp;
}
//祖父节点右节点为null-或者-祖父节点右节点为黑色会进入一下方法.
else {
//判断当前节点是否在父节点的右节点上,在treeify中其实已经赋值过了,这里加判断在循环这棵树中作用.
if (x == xp.right) {
//成立:进行左旋,把红黑树进行调整组成树返回.详解请看:1.7
//把当前树和xp(父节点)传入,这x=xp x是被赋值成xp父节点了.
//左旋详情请看下面1.7rotateLeft()方法
//这里的左旋传的是父节点
root = rotateLeft(root, x = xp);
/*进行右旋以后xp(当前节点的父节点)->x(当前节点),
但是这里用x(当前节点)=xp(当前父节点)已经转换了.
形成了x(当前父节点)->xp(当前节点)那么x.partent也就是传入的当前节点.*/
//一定要理解这个x是最后一个节点.
//以下的情况是:左旋过后的情况
/*
x:为父节点
xp:当前节点
xpp:祖父节点
*/
//这里三元运算符判断父节点是否为null。如果为null,祖父也就为null。
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//判断祖父节点不为null就进入下面方法,如果为null就继续for循环x=xp。
if (xp != null) {
//父节点变为红色
xp.red = false;
//祖父节点是否为null,不为null就进行左旋,为null就进行for循环x=xp。循环父节点。
//动态图1.4
if (xpp != null) {
//把祖父节点变为红色
xpp.red = true;
//进行右旋 这里的右旋传的是祖父节点
//右旋详情请看下面1.8rotateRight()方法
root = rotateRight(root, xpp);
}
}
}
}
//判断如果父节点在祖父节点的又变进入此方法
else {
//判断祖父节点的左节点不为null且祖父节点的左节点为红色,进入下面方法.
//在上面的判断中依然有右节点不为null且右节点为红色的判断,不管父节点在做还是在又,都要进行父节点和叔叔节点为黑色.
if (xppl != null && xppl.red) {
//叔叔节点为黑色
xppl.red = false;
//父节点为黑色
xp.red = false;
//祖父节点为红色
xpp.red = true;
//把祖父节点赋值给x,继续循环
x = xpp;
}
//如果左节点为null或者左节点为黑色,会进入以下方法
else {
if (x == xp.left) {
//x(当前节点)->xp(当前节点的父节点),x(当前节点)=xp(当前节点的父节点).这是进行右旋的时候
//这里的右旋传的是父节点
//左旋详情请看下面1.8rotateRight()方法
root = rotateRight(root, x = xp);
/*进行右旋以后xp(当前节点的父节点)->x(当前节点),
但是这里用x(当前节点)=xp(当前父节点)已经转换了.
形成了x(当前父节点)->xp(当前节点)那么x.partent也就是传入的当前节点.*/
//一定要理解这个x是最后一个节点.
//以下的情况是:左旋过后的情况
/*
x:为父节点
xp:当前节点
xpp:祖父节点
*/
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//判断付姐点不为null
if (xp != null) {
//父节点为黑色
xp.red = false;
//祖父节点不为null
if (xpp != null) {
//祖父节点为红色
xpp.red = true;
//进行右旋,这里的左旋传的是祖父节点
//左旋详情请看下面1.7rotateLeft()方法
root = rotateLeft(root, xpp);
}
}
}
}
}
}
/*
通过上面的方法,可以总结出一些规律,如果当前节点在父节点的左边,那么就会进行右旋,如果当前节点在父节点右边,那么就会进行左旋.而且进行旋转是看当前节点和父节点和祖父节点,当这三个组成好后,如果还有曾父节点,就会继续for循环曾父节点上层的节点.依次到最后根节点完毕.
*/
**
图片:1.1
图片:1.2
图片:1.3
图片:1.4 这里先进行左旋,曾祖父节点0050不为null,然后再进行右旋.最后形成树形结构.
1.7 rotateLeft()左旋方法
**
//p节点是父节点还是祖父节点,要看balanceInsertion()方法中传入的是父节点还是祖父节点,进行左旋
// Red-black tree methods, all adapted from CLR
//root为树,p为父节点.
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
//声明树节点类型TreeNode<K,V> r,pp,rl.
TreeNode<K,V> r, pp, rl;
//赋值并判断,r = p.right,父节点的右节点赋值给r.r(当前节点)
//判断父节点不等于null且父节点的右节点不为null,x == xp.right这里其实已经判断过一次.
if (p != null && (r = p.right) != null) {
//赋值并判断,当前节点的左节点赋值给父节点的右节点赋值给rl.判断是否等于null.
if ((rl = p.right = r.left) != null)
//当前节点的做节点父节点变为父节点.
rl.parent = p;
//赋值并判断,p(父节点).parent的父节点赋值给r.parent的指向等于祖父节点pp.
//判断祖父节点是否为null.
if ((pp = r.parent = p.parent) == null)
//如果祖父节点为null的话,root根节点为黑色
(root = r).red = false;
//判断祖父节点的左节点是否等于祖父节点
else if (pp.left == p)
//成立,祖父节点的左节点等于当前节点.
pp.left = r;
else
//否则祖父节点的右节点等于当前节点
pp.right = r;
//当前节点的左节点等于父节点了.
r.left = p;
//父节点指向当前节点..
p.parent = r;
}
return root;
}
**
1.8 rotateRight()右旋方法
**
//p节点是父节点还是祖父节点,要看balanceInsertion()方法中传入的是父节点还是祖父节点,进行右旋
//root为树,p为父节点.
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
//声明树节点l,pp,lr.
TreeNode<K,V> l, pp, lr;
//判断父节点的左节点不为null且把父节点的左节点赋值给l
if (p != null && (l = p.left) != null) {
//把l的右节点赋值给p的左节点赋值给lr,也就是把父节点的左节点设为null了.
//如果不为null,那么l节点的右节点成了父节点的左节点了.
if ((lr = p.left = l.right) != null)
lr.parent = p;
//把父节点的指向赋给l节点的指向为祖父节点.
//如果为null,那么l节点为黑色
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
//如果祖父节点的右节点为父节点
else if (pp.right == p)
//那么祖父节点的右节点为l节点
pp.right = l;
else
//否则祖父节点的左节点为l节点
pp.left = l;
//l节点的右节点为父节点.
l.right = p;
//父节点的指向为l节点
p.parent = l;
}
//返回root树
return root;
}
moveRootToFront()方法
/**
* Ensures that the given root is the first node of its bin.
* 确保给定的根是其bin的第一个节点。
*
* tab 数组
* root 红黑树
*/
/**
* 把红黑树的根节点设为 其所在的数组槽 的第一个元素
* 首先明确:TreeNode既是一个红黑树结构,也是一个双链表结构
* 这个方法里做的事情,就是保证树的根节点一定也要成为链表的首节点
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) { // 根节点不为空 并且 HashMap的元素数组不为空
int index = (n - 1) & root.hash; // 根据根节点的Hash值 和 HashMap的元素数组长度 取得根节点在数组中的位置
TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; // 首先取得该位置上的第一个节点对象
if (root != first) { // 如果该节点对象 与 根节点对象 不同
Node<K,V> rn; // 定义根节点的后一个节点
tab[index] = root; // 把元素数组index位置的元素替换为根节点对象
//从这里开始把root节点从链表中剥离出来
TreeNode<K,V> rp = root.prev; // 获取根节点对象的前一个节点
if ((rn = root.next) != null) // 如果后节点不为空
((TreeNode<K,V>)rn).prev = rp; // root后节点的前节点指向到 root的前节点相当于把root从链表中摘除
if (rp != null) // 如果root的前节点不为空
rp.next = rn; // root前节点的后节点 指向到 root的后节点
//判断当前坐标中的first是否为null.如果为null,不进入下面方法.
if (first != null) // 如果数组该位置上原来的元素不为空
first.prev = root; // 这个原有的元素的 前节点 指向到 root,相当于root目前位于链表的首位
//如果first为null,那么root就从链表中剥离出来了.
root.next = first; // 原来的第一个节点现在作为root的下一个节点,变成了第二个节点
root.prev = null; // 首节点没有前节点
}
/*
* 这一步是防御性的编程
* 校验TreeNode对象是否满足红黑树和双链表的特性
* 如果这个方法校验不通过:可能是因为用户编程失误,破坏了结构(例如:并发场景下);也可能是TreeNode的实现有问题(这个是理论上的以防万一);
*/
assert checkInvariants(root);
}
}