HashMap底层原理解析JDK1.7与JDK1.8(二)

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); 
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值