本文共 21520 字,大约阅读时间需要 71 分钟。
接下来就来看下源码中,没有转换为红黑树前的增删查,与转换为红黑树的增删查。
红黑树演示网站:
新增
/** * 添加方法 */ public V put(K key, V value) { //调用putVal return putVal(hash(key), key, value, false, true); }
/** * * @param hash 新建K-V的key的哈希值 * @param key 新增的key * @param value 新增的value * @param onlyIfAbsent 如果是true,不改变键值对中的旧值 * @param evict 驱逐出去,这个在HashMap不用管,作用在LinkedHashMap * @return V */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; //桶 Node<K,V> p; //当前桶位置元素的key int n, i; //当前元素位置
//1、判断table是否为空,或者table长度是否为0 if ((tab = table) == null || (n = tab.length) == 0) //1.2、在1的条件成立的情况下,调用resize()对容器进行扩容 n = (tab = resize()).length;
//2、计算插入元素在hash桶中的位置,并且这个位置如果没有元素,则将数据封装成一个Node对象 if ((p = tab[i = (n - 1) & hash]) == null) //2.1、新建一个Node tab[i] = newNode(hash, key, value, null);
else { //3、如果这个位置有元素则执行以下逻辑 Node<K,V> e; //当前桶位置元素 K k; //当前桶位置元素的key
//3.1、如果这个位置的元素的key和要插入的一样(hashCode一样,equals一样) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //3.1.1、那么就替换一下,记录该节点到变量e e = p;
//3.2、判断该节点是否是树类型的节点 else if (p instanceof TreeNode) //3.2.1、是树类型节点,就按照树类型的添加方法进行添加 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//3.3、第一个元素即不是和插入的节点相同,又不是树类型的节点,那么则遍历链表 else { //3.3.1、遍历链表 for (int binCount = 0; ; ++binCount) { //3.3.1.1、先将下一个节点赋给e,如果该节点为空,则表示已经遍历到链表的最后了 if ((e = p.next) == null) { //3.3.1.1.1、新建一个节点并加入到末尾 p.next = newNode(hash, key, value, null);
//3.3.1.1.2、此时判断链表长度是否大于8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //3.3.1.1.2.1、链表大于8,则将链表转换为红黑树 treeifyBin(tab, hash); break; } //3.3.1.2、如果某个节点和要插入的节点相同,则跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break;
//3.3.1.3、由于在3.3.1.1中p已经把下一个节点给了,e因此, //此时p重新拿到下一个节点,重新进入循环,在回到3.3.1.1中又会把下一个节点给到e,不断循环 p = e; } }
//3.4、此时e节点中记录的是hashMap中与要插入的节点相同的节点,在这里统一进行处理 if (e != null) { //3.4.1、记录旧的value值 V oldValue = e.value; //3.4.2、通过对onlyIfAbsent与oldValue的值判断是否需要覆盖,需要则覆盖 if (!onlyIfAbsent || oldValue == null) e.value = value;
//此方法与LinkedHashMap相关,是一个回调方法,在HashMap中没有任何作用 afterNodeAccess(e);
//3.4.3、返回旧的值 return oldValue; } } //4、操作+1,与之前的ArrayList的Fail-Fast机制是一样的 ++modCount; //5、判断是否需要进行扩容 if (++size > threshold) //5.1、调用resize()方法进行扩容 resize();
//6、此方法也是与LinkedHashMap相关的方法,在HashMap中没有任何作用 afterNodeInsertion(evict); return null; } |
putVal()方法中的2.1内,新建一个Node。
// Create a regular (non-tree) node Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } |
putVal()扩容方法
/** * 扩容:初始化或翻倍容量大小 * 如果存储容量为null,则根据threshold变量中的初始化capacity的值来分配table内存, * 如果表不为null,就使用2的幂来扩容,每个bin元素要么还是在原来的桶,要么在2的幂中 * 注:在实例化HashMap时,capactity其实是存放在成员变量的threshold中,HashMap中是没有capacity这个变量的 * @return Node<K,V>[] */ final Node<K,V>[] resize() { //记录旧的Table //1、记录旧的数组table Node<K,V>[] oldTab = table;
//2、判断原来的table是否为空,如果为空,旧的容量为0,否则旧容量为旧数组的长度 int oldCap = (oldTab == null) ? 0 : oldTab.length;
//3、记录旧阈值 int oldThr = threshold;
//4、新变量 int newCap, //记录新的容量 newThr = 0; //记录新的阈值
//5、判断原来容量是否大于0,由于HashMap是在第一次put才初始化,因此此方法也是判断table是要扩容还是要初始化,大于0就是已初始化过了 if (oldCap > 0) { //5.1、如果旧容量大于或等于最大容量值 if (oldCap >= MAXIMUM_CAPACITY) { //如果原来的容量大于0且大于等于最大值 //5.1.1、将阈值设置为最大值,并返回原来的容量大小,代表table已经不能再扩容了 threshold = Integer.MAX_VALUE; return oldTab; }
//5.2、如果当前容量*2 < 最大容量 和 大于默认初始化容量(16)并将原容量<<1(相当于*2)复制给newCap else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //5.2.1、符合5.2的条件的话,证明是要进行扩容,而不是初始化,将原来扩容阈值<<1(相当于 * 2)赋值给newThr newThr = oldThr << 1; }
//6、如果说oldCap(原来容量为0),说明hashMap没有被初始化过,而且原来的阈值大于0,回去看看hashMap的构造方法 else if (oldThr > 0) //6.1、将新的容量设置为原来的阈值 newCap = oldThr;
//7、如果5和6都不符合条件,说明在新建hashMap时,是没有指定初始化值,或者是将初始化大小设置为0 else { //7.1、将新容量设置为默认值的16 newCap = DEFAULT_INITIAL_CAPACITY;//设为默认值16 //7.2、将阈值设置为16*0.75 = 12 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }
//8、如果新的阈值为0,标识hashMap是给定了capacity且不为0 //可能一:进入此if有两种可能if (oldCap > 0)后,不满足里面其中的2个if,说明是进行扩容,而且oldCap(旧容量)小于16 //可能二:进入else if (oldThr > 0)这个条件中,说明是第一次put if (newThr == 0) { //8.1、设置为newCap*loadFactor或是最大值 float ft = (float)newCap * loadFactor;
//8.2、计算扩容阀值 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); }
//9、设置当前阈值为新计算出来的阈值 threshold = newThr;
@SuppressWarnings({ "rawtypes","unchecked"}) //10、以新的容量去新建一个table数组(可能是初始化,也可能是扩容) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //11、当前数组table,为10的新的数组table table = newTab;
//12、如果oldTab不为空,说明是扩容,否则直接返回新的table即可 if (oldTab != null) {
//进入扩容操作
//12.1、遍历hashMap中的桶 for (int j = 0; j < oldCap; ++j) { //12.1.1、当前桶的元素 Node<K,V> e;
//12.1.2、遍历每一个hash桶中的元素,并记录此节点到变量e if ((e = oldTab[j]) != null) { //12.1.2.1、将原有位置设置为空 oldTab[j] = null;
//12.1.2.2、如果下一个元素为空,说明只有一个元素 if (e.next == null) //12.1.2.1.1、计算新的位置,并插入 newTab[e.hash & (newCap - 1)] = e;
//12.1.2.3、如果是树节点,则转化为树的扩容操作 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//12.1.2.4、正式扩容操作 else { //12.1.2.4.1、将每个桶中的元素分为两类,扩容后的位置和原来相同则记录到loHead和loTail这个链表中 Node<K,V> loHead = null, loTail = null; //12.1.2.4.2、扩容后与原来位置不同的,则记录到hiHead和hiTail这个链表中 Node<K,V> hiHead = null, hiTail = null;
//12.1.2.4.3、当前节点的下一个节点 Node<K,V> next; do { //12.1.2.4.3.1、记录当前节点的下一个节点 next = e.next;
//12.1.2.4.3.2、如果e的hash值与老表与运算为0,则扩容后的索引位置保持与老的一致 //注:table是2倍扩容的,只看hash值和oldCap进行擦欧洲哦,结果为0,那么还是原来的索引位,否则索引位 = 原索引位 + oldCap if ((e.hash & oldCap) == 0) { //12.1.2.4.3.2.1、如果低位尾节点的loTail为空 if (loTail == null) //12.1.2.4.3.2.1.1、设置新节点为低位头节点 loHead = e; //12.1.2.4.3.2.2、如果低位尾节点的loTail不为空 else //12.1.2.4.3.2.2.1、将新增节点放到末尾 loTail.next = e; //12.1.2.4.3.2.3、设置loTail为新增的节点 loTail = e; } //12.1.2.4.3.3、如果e的hash值与老表的与运算不为0,扩展后的索引在原来的索引上加上原来数组的大小 else { //12.1.2.4.3.3.1、如果高位尾节点为null if (hiTail == null) //12.1.2.4.3.3.1.1、设置新节点为高位头节点 hiHead = e;
//12.1.2.4.3.3.2、如果高位尾节点不为null else //12.1.2.4.3.3.2.1、将新的节点放到末尾 hiTail.next = e; //12.1.2.4.3.3.3、设置hiTail为新增的节点 hiTail = e; } } while ((e = next) != null);
//如果低位尾节点不为空 if (loTail != null) { loTail.next = null; //还是原来的索引位置 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; //索引位置变为原索引位置 + oldCap newTab[j + oldCap] = hiHead; } } } } } //返回新的扩容数组 return newTab; } |
链表转红黑树
putVal()方法中的3.2内,判断是一个树类型的节点,那么则调用树节点的添加方法,在调用之前要先把链表转换为树结构。
/** * 容器链表长度(TREEIFY_THRESHOLD)大于8,就转换为红黑树 * @param tab 元素的数组 * @param hash 要增加的键值对的key的hash值 */ final void treeifyBin(Node<K,V>[] tab, int hash) { //节点数组的长度 int n, //hash对应的数组下标 index; //用于循环的迭代变量,代表当前节点 Node<K,V> e;
//1、如果元素数组为空,或者数组长度小于树结构化的最小限制 //MIN_TREEIFY_CAPACITY 默认值为64,这个值是指如果元素数组长度小于这个值,就没有必要进行结构转换 //当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结构相同(并不是因为这些key的hash值相同)。 //因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); //扩容
//2、如果元素数组长度已经大于等于了MIN_TREEIFY_CAPACITY,那么久有必要进行结构转换了 else if ((e = tab[index = (n - 1) & hash]) != null) { //2.1、定义首尾节点 TreeNode<K,V> hd = null, //首节点 tl = null; //尾节点
//3、循环,把原单向链表转换为双向链表 do { //3.1、将该节点转换为树节点 TreeNode<K,V> p = replacementTreeNode(e, null); //3.2、如果尾节点为空,说明还没有根节点 if (tl == null) //3.2.1、首节点(根节点)指向当前节点 hd = p;
//3.3、尾节点不为空,以下两行是一个双向链表结构 else { //3.3.1、当前树节点的前一个节点指向尾节点 p.prev = tl; //3.3.2、尾节点的后一个节点指向当前节点 tl.next = p; } //3.4、把当前节点设置为尾节点 tl = p; } while ((e = e.next) != null); //3.5、继续遍历链表
//3、如果指定位置的头节点不为空,则进行树形化 //注:到目前为止也只是把Node对象转换为TreeNode对象,把单向链表转换为双向链表 if ((tab[index] = hd) != null) //根据链表创建树结构(JDK1.8,红黑树) hd.treeify(tab); } } |
/** * 链表转换为树 * @param tab 存储K-V的数组 */ final void treeify(Node<K,V>[] tab) { //1、树的根节点 TreeNode<K,V> root = null;
//2、遍历链表,x指向当前节点,next指向下一个节点 for (TreeNode<K,V> x = this, next; x != null; x = next) { //2.1、当前节点的下一个节点 next = (TreeNode<K,V>)x.next; //将当前节点的左右节点都置为null x.left = x.right = null;
//2.2、如果根节点为空 if (root == null) { //2.3.1、当前节点的父节点也为空 x.parent = null; //2.3.2、当前节点的颜色为红色 x.red = false; //2.3.3、根节点为当前节点 root = x; } //2.3、如果根节点不为空,即已存在根节点 else { //2.3.1、取得当前链表节点的key K k = x.key;
//2.3.2、取得当前链表节点的hash值 int h = x.hash;
//2.3.3、定义key所属的Class Class<?> kc = null;
//2.3.4、从根节点开始遍历,没有设置边界,只能从内部跳出 for (TreeNode<K,V> p = root;;) { int dir, //标识左右方向 ph; //当前树节点的hash值
//2.3.5、当前树节点的key K pk = p.key;
//2.3.6、如果当前树节点hash值 大于 当前链表节点的hash值 if ((ph = p.hash) > h) //2.3.6.1、当前链表节点放到当前树节点的左边 dir = -1;
//2.3.7、如果当前树节点hash值 小于 当前链表节点的hash值 else if (ph < h) //2.3.7.1、当前链表节点放到当前树节点的右边 dir = 1;
//2.3.8、如果两个节点的key的hash值相等,那么还要通过其他方式在进行比较 //如果当前链表节点的key实现了Comparable接口,并且当前树节点和链表节点时相同Class的实例, //那么通过Comparable的方式再比较,如果还是相等最后再通过tieBreakOrder在比较一次 else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk);
//2.3.9、保存当前树节点 TreeNode<K,V> xp = p;
//2.3.10、p被赋值为当前p的左右节点,所以xp为下面p的父节点 //如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就把要添加的元素作为当前节点的左节点,如果不为空,进行下一轮继续比较 //如果dir小于等于0,那么看当前节点的右节点是否为空,如果为空,就把要添加的元素作为当前节点的右节点,如果不为空,进行下一轮继续比较 //如果上述两条当中有一个字节点不为空,if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较 //备注: //如果当前节点不是叶子节点,那么最终会以当前树节点的左或右为起始节点,在从GOTO1处开始重新寻找自己(当前链表节点)的位置 //挂载之后还需要重新将树进行平衡,平衡之后就可以针对下一个链表节点进行处理了 if ((p = (dir <= 0) ? p.left : p.right) == null) { //2.3.10.1、当前链表节点作为当前树节点的子节点 x.parent = xp;
//2.3.10.2、小于0 if (dir <= 0) //2.3.10.2.1、挂载到左边 xp.left = x;
//2.3.10.3、大于0 else //2.3.10.3.1、挂载到右边 xp.right = x;
//2.3.10.4、重新平衡 root = balanceInsertion(root, x); break; } } } }
//3、把所有的链表节点都遍历完之后,最终构造出阿里的 树,可能经历多个平衡操作,根节点目前到底是链表哪个节点是不确定的 //因为要基于树来做查找,所以应该吧tab[N]得到的对象一定根是节点对象,而目前只是链表的第一个节点对象,所以要做相应的处理。 //把红黑树的根节点设为,其所在的数组槽的第一个元素。 //明确TreeNode即是一个红黑树结构,也是一个双链表结构 //这个方法做的事情就是保证树的根节点一定也要成为链表的首节点 moveRootToFront(tab, root); } |
/** * 这个方法是用于a和b两个对象的比较的,是用于这两个对象的hash值比较。 * 这个树的比较,不要求完全有序,只要插入时,使用相同的的规则保持平衡即可, * 即确定插入的节点要么是树的左节点要么是右节点,不然就无法满足二叉树结构了,所以不是返回大于0,就是返回小于0,不会返回0。 * * @param a 根据treeify方法的使用来看,这个是当前链表节点的key * @param b 根据treeify方法的使用来看,这个是当前树节点的key * @return int 1在右,-1在左,即a>b返回1,a<b返回-1 */ static int tieBreakOrder(Object a, Object b) { int d; //先比较这个2个类的类名,类名是字符串对象,就按字符串的比较规则。 if (a == null || b == null || (d = a.getClass().getName(). compareTo(b.getClass().getName())) == 0) //如果两个对象是同一个类型,那么调用本地方法为这2个对象生产hashCode值, //在进行比较,hashCode小于或相等的话,则返回-1 d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1); return d; } |
/** * 树插入节点后,需要重新平衡 * 黑红树的特点: * 每个节点不是红色就是黑色 * 根节点是黑色 * 每个NIL/NULL的叶子节点是黑色 * 如果一个节点时红色,子节点必然是黑色 * 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点 * @param root 当前根节点 * @param x 新插入的节点 * @return TreeNode<K,V> */ static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) { //1、新插入的节点标记为红色 x.red = true;
//2、在循环中的首部就定义了变量,没有设置控制条件,只能从内部跳出 //xp:当前节点的父节点 //xpp:当前节点的爷节点 //xppl:当前节点的左叔叔节点 //xppr:当前节点的右叔叔节点 for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//2.1、当前节点的父节点是否为空 if ((xp = x.parent) == null) { //2.1.1、为空就将当前节点置为黑节点,并返回 x.red = false; return x; } //2.2、当前节点的父节点颜色为黑色,或当前节点的爷节点为空(说明当前节点就是根节点),那么就不用调整了 else if (!xp.red || (xpp = xp.parent) == null) return root;
//2.3、如果当前节点的爷节点的左孩子,是当前节点的父节点 //经过了前面的判断了,说明了,父节点是红色了。 if (xp == (xppl = xpp.left)) { //2.3.1、当前节点的爷爷节点的右叔叔节点不为空,并且节点颜色是红色 if ((xppr = xpp.right) != null && xppr.red) { //2.3.1.1、右叔叔置为红色 xppr.red = false; //2.3.1.2、父节点置为红色 xp.red = false; //2.3.1.3、爷节点置为红色 xpp.red = true; //2.3.1.4、开始新一轮的循环,将爷节点当做处理的起始节点处理 x = xpp; }
//2.3.2、当前节点的爷爷节点的右叔叔节点为空或节点颜色是黑色 else { //2.3.2.1、如果当前节点是父节点的右孩子 if (x == xp.right) { //2.3.2.1.1、父节点左旋,此时树结构为xpp -> 左xp -> 右x root = rotateLeft(root, x = xp); //2.3.2.1.2、获取爷节点 xpp = (xp = x.parent) == null ? null : xp.parent; } //2.3.2.2、如果父节点不为空 if (xp != null) { //2.3.2.2.1、父节点的颜色为黑色 xp.red = false;
//2.3.2.2.2、爷节点不为空 if (xpp != null) { //2.3.2.2.2.1、颜色为红色 xpp.red = true; //2.3.2.2.2.2、将爷节点进行右旋 root = rotateRight(root, xpp); } } } } //2.4、如果当前节点的爷节点的右孩子,是当前节点的父节点 else { //2.4.1、如果左叔叔是红色 if (xppl != null && xppl.red) { //2.4.1.1、左叔叔置为黑色 xppl.red = false; //2.4.1.2、父节点置为黑色 xp.red = false; //2.4.1.3、爷节点置为红色 xpp.red = true; //2.4.1.4、开始新一轮的循环,将爷节点当做处理的起始节点处理 x = xpp; } //2.4.2、当前节点的爷爷节点的左叔叔节点为空或节点颜色是黑色 else { //2.4.2.1、如果当前节点时个左孩子 if (x == xp.left) { //2.4.1.1.1、针对父节点做左旋 root = rotateRight(root, x = xp); //2.4.1.1.2、获取爷节点 xpp = (xp = x.parent) == null ? null : xp.parent; } //2.4.1.2、如果父节点不为空 if (xp != null) { //2.4.1.2.1、将父节点置为黑色 xp.red = false; //2.4.1.2.2、如果爷节点不为空 if (xpp != null) { //2.4.1.2.2.1、爷节点置为红色 xpp.red = true; //2.4.1.2.2.2、将爷节点左旋 root = rotateLeft(root, xpp); } } } } } }
/** * 节点左旋 * px px * / / * x y * / \ --(左旋)-- / \ * lx y x ry * / \ / \ * ly ry lx ly * @param root 根节点 * @param p 要左旋的节点 * @return TreeNode<K,V> */ static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) { TreeNode<K,V> r, //当前节点的右节点 pp, //当前节点的父节点 rl; //当前节点的右节点的左节点 //1、要左旋的节点以及要进行左旋的节点的右孩子不为空 if (p != null && (r = p.right) != null) { //1.1、要左旋的节点的右孩子的左节点赋给rl,并且判断不为空 if ((rl = p.right = r.left) != null) //1.1.1、设置右孩子的左节点rl和要左旋的节点的父子关系,之前只是父节点认了孩子,孩子没有答应,现在孩子也认了爹 rl.parent = p;
//1.2、将要左旋的节点的右孩子的父节点 指向 要左旋的节点的父节点,相当于右孩子提升了一层 if ((pp = r.parent = p.parent) == null) //1.2.1、此时如果父节点为空,说明了r已经是顶层节点,应该作为root,并且置为黑色 (root = r).red = false;
//1.3、如果父节点不为空,并且要左旋的节点是个孩子 else if (pp.left == p) //1.3.1、设置r和父节点的父子关系,之前只是父节点认了孩子,孩子没有答应,现在孩子也认了爹 pp.left = r;
//1.4、要左旋的节点是个右孩子 else //1.4.1、设置r和父节点的关系 pp.right = r; //1.5、要左旋的节点作为他的右孩子的左节点 r.left = p; //1.6、要左旋的节点的右孩子,为他的父节点 p.parent = r; } //2、返回根节点 return root; } /** * 节点右旋 * py py * / / * y x * / \ --(右旋)-- / \ * x ry lx y * / \ / \ * lx rx rx ry * @param root 根节点 * @param p 要左旋的节点 * @return TreeNode<K,V> */ static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) { TreeNode<K,V> l, //当前节点的左节点 pp, //当前节点的父节点 lr; //当前节点的左节点的右节点
//1、如果要右旋的节点不为空,以及要右旋的的节点的左孩子不为空 if (p != null && (l = p.left) != null) { //1.1、要右旋的节点的左孩子的右节点,赋给 要右旋节点的左孩子,节点lr if ((lr = p.left = l.right) != null) //1.1.1、设置当前节点的左节点的右节点和要右旋的节点的父子关系 lr.parent = p;
//1.2、如果要右旋的节点的左孩子的父节点 指向 要右旋的节点的父节点,相当于左孩子提升一层 //并且此时如果父节点为空,说明l已经是顶层节点,应该作为root if ((pp = l.parent = p.parent) == null) //1.2.1、当前父节点置为root节点,并置为黑色节点 (root = l).red = false;
//1.3、如果父节点不为空,并且要右旋的节点是个右孩子 else if (pp.right == p) //1.3.1、设置l和父节点的父子关系 pp.right = l;
//1.4、要右旋的节点是个左孩子 else //1.4.1、确立关系 pp.left = l; //1.5、要右旋的节点作为孩子的右节点 l.right = p; //1.6、要右旋的节点的父节点指向左孩子 p.parent = l; } //2、返回根节点 return root; } |
/** * 为了保证每次红黑树的根节点都在链表的第一个位置,当删除或增加红黑树的节点的时候, * root节点在双链表中的位置可能会有变动,在操作完成之后,需要moveRootToFront方法进行调整 * @param tab hashMap下的table数组 * @param root 调整红黑树之后的root节点 */ static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) { //hashMap的table数组的长度 int n;
//根节点不为空,并且HashMap的元素数组不为空 if (root != null && tab != null && (n = tab.length) > 0) { //根据节点的hash值和HashMap的元素数组长度,取得根节点在数组中的位置 int index = (n - 1) & root.hash;
//首先取得该位置上的第一个节点的对象 TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
//如果该节点和根节点对象不同 if (root != first) {
//根节点的下一个节点 Node<K,V> rn;
//把元素数组索引位置的元素替换为根节点对象 tab[index] = root;
//获取根节点对象的上一个节点 TreeNode<K,V> rp = root.prev;
//如果根节点的后下个节点不为空 if ((rn = root.next) != null) //root节点的下一个节点的上一个节点,为root节点的上一个节点,相当于把root从链表摘除 ((TreeNode<K,V>)rn).prev = rp;
//如果根据节点的上一个节点不为空 if (rp != null) //根节点的上一个节点的下一个节点,为根节点的下一个节点 rp.next = rn;
//如果数组该位置上原来的元素不为空 if (first != null) //这个原来的元素的上一个节点指向root,相当于root目前位于链表的首位 first.prev = root;
//原来的第一个节点现在作为root的下一个节点,变成了第二个节点 root.next = first; //首节点没有上一个节点 root.prev = null; }
//校验TreeNode对象是否满足红黑色和双向链表的特性 //如果没有通过这个方法的校验,可能是编程失误,破坏了结构(并发的Fast-Fial机制),也可能TreeNode的实现问题 assert checkInvariants(root); } } |
红黑树的新增
/** * 树结构类型的添加: * 将插入节点的hash值与每个节点的hash值进行比较, * 如果是小于下次插入节点就与当前节点的左子节点对比,反之则与右子节点对比 * 直到当前节点的(左或右)子节点为null,将其插入。 * 每当插入一次节点都会调用balanceInsertion()方法,进行树平衡操作 * @param map 当前节点所在hashMap对象 * @param tab hashMap中的Node数组节点 * @param h 需要存放Key的hash值 * @param k 需要存放的Key * @param v 需要存放的value * @return TreeNode<K,V> */ final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) { //key的类型 Class<?> kc = null; //是否遍历过树了,未必是从根节点遍历的, //但是遍历路径上一定已经包含后续要对比的所有节点 boolean searched = false;
//1、获取该节点的根节点,如果自身为空,那么自身就是根节点了 TreeNode<K,V> root = (parent != null) ? root() : this;
//2、以根节点为开始进行遍历,没有终止条件,只能从内部退出 for (TreeNode<K,V> p = root;;) { //dir树的左右方向 int dir, //当前节点的hash值 ph; //当前节点的key值 K pk;
//2.1、当前节点的hash值大于需要存放的Key的hash值 if ((ph = p.hash) > h) //2.1.1、要添加的元素防止在当前节点的左侧 dir = -1;
//2.2、当前节点的hash值小于需要存放的Key的hash值 else if (ph < h) //2.2.1、要添加的元素放在当前节点的右侧 dir = 1;
//注:左小、右大
//2.3、当前节点的hash值等于需要存放key的hash值,直接返回树节点 //hash相同,equals相同 else if ((pk = p.key) == k || (k != null && k.equals(pk))) //2.3.1、返回当前节点对象,在外层方法对V进行写入 return p;
//2.4、如果k没有实现Comparable接口,或者k(需要存放的Key)和pk(当前节点的key值)相同 //即hash相等,equals不相等 else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) {
//注、需要存放的key没有实现Comparable接口,或者实现了Comparable接口并且和当前节点的键对象比较之后相等(仅限第一次循环)
//searched说明: //标识是否已经对比过当前节点的左右点了 //如果没有遍历过,那么递归遍历对比,看是否能够得到那个键对象eqauls相等的节点 //如果得到了键的eqauls相等的节点就返回 //如果还是没有键的equals相等的节点,那说明应该创建一个新的节点
//2.4.1、如果还没有对比过当前节点的所有子节点 if (!searched) { //要返回的节点 TreeNode<K,V> q, //子节点 ch;
//2.4.1.1、标识已经遍历过一次 //第一次遍历就置为false,并且此时第一次的节点为根节点, //也就是说默认第一次会判断hash值或key值来决定左右遍历查找目标key searched = true;
//2.4.1.2、如果当前节点的左节点不为空,就从左节点开始遍历查询,否则就从右节点开始查询 //红黑树也是个二叉树,所以只要沿着左右两侧查找就可以了 //这是个短路运算,如果从左侧已经找到了,右侧就不需要遍历了 if (((ch = p.left) != null && //左 (q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null && //右 (q = ch.find(h, k, kc)) != null)) //找到指定key键对应的,直接返回 return q; } //2.4.2、走到这,说明了便利了所有子节点也没有知道和当前键equals相等的节点, //即hash值相同且根据key值比较大小来遍历查询并没有找到目标对象, //这时返回k(需要存放的Key)和pk(当前节点的key值)两者系统hash值大小比较结果, //k小于pk返回-1,否则返回1给dir,相当于比较系统hash值来决定向左查询还是向右查询 dir = tieBreakOrder(k, pk); }
//2.5、定义xp指向当前节点 TreeNode<K,V> xp = p;
//注:左小、右大
//2.6、p被赋值为当前p的左右节点,所以xp为p下面p的父节点 //如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就把要添加的元素作为当前节点的左节点,如果不为空,进行下一轮继续比较 //如果dir小于等于0,那么看当前节点的右节点是否为空,如果为空,就把要添加的元素作为当前节点的右节点,如果不为空,进行下一轮继续比较 //如果上述两条当中有一个字节点不为空,if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较 if ((p = (dir <= 0) ? p.left : p.right) == null) {
//2.6.1、当前节点的next节点 Node<K,V> xpn = xp.next;
//2.6.2、构建一个存放进来key,value的树节点,并且下一节点指向当前节点的下一个节点xpn,上一节点指向xp,且父节点也指向xp //构建一个存放进来的K-V的树节点,并且下一节点指向当前节点的下一个节点xpn,上一节点指向xp,且父节点也指向xp TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//注:左小、右大
//2.6.3、左边指向这个新的树节点 if (dir <= 0) xp.left = x;
//2.6.4、右边指定这个新的树节点 else xp.right = x;
//2.6.5、下一个节点指向x节点 xp.next = x;
//2.6.6、x的上一节点指向xp,且父节点也指向xp x.parent = x.prev = xp;
//2.6.7、如果当前x节点下一节点不为空,则将下一节点的上一节点设置为当前节点的x if (xpn != null) //2.6.7.1、那么原来的next节点的前节点指向新的树节点 ((TreeNode<K,V>)xpn).prev = x;
//2.6.7.2、重新平衡,以及新的根节点置顶 moveRootToFront(tab, balanceInsertion(root, x)); return null; } } } |
红黑树节点的添加方法中,并没有扩容方法,但是之前在解析putVal()方法时,看到的是扩容时在这里判断,扩容的方法是resize(),并且对resize()这个方法做解析时,可以发现在(12.1.2.3、如果是树节点,则转化为树的扩容操作)中,已经判断是否是树结构,如果是树结构就对树进行扩容,那么现在来看下这个树结构的扩容方法。
/** * 扩容后的红黑树的hash分布,只可能存在于原索引位置、原索引位置+oldCap *在resize()方法调用split()方法传入的参数 *((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) { //1、拿到调用这个方法的节点,即当前节点 TreeNode<K,V> b = this;// 拿到调用此方法的节点
//2、将每个桶总的元素分为两类,扩容后的位置和原来相同的则记录到loHead、loTail这个链表 TreeNode<K,V> loHead = null, loTail = null;
//3、将每个桶中的元素分为两类,扩容后的位置和原来的不同,则记录到hiHead、hiTail这个链表中 TreeNode<K,V> hiHead = null, hiTail = null;
//4、初始值为0,用于决定了红黑树是否要转回链表 int lc = 0, //桶的红黑树节点达到了大于UNTREEIFY_THRESHOLD,转换为红黑树 hc = 0; //桶的红黑树节点达到了6(UNTREEIFY_THRESHOLD),转换为链表
//5、以当前节点为开始,遍历整个红黑树的节点,e=当前节点 for (TreeNode<K,V> e = b, next; e != null; e = next) { //5.1、拿到当前节点的下一个节点,赋予给变量next next = (TreeNode<K,V>)e.next;
//5.2、将e节点的下一个节点置为空,给垃圾回收器回收 e.next = null;
//5.3、如果e的hash值和老表的容量进行与运算为0,则扩容后的索引位置跟老的标索引位置一样 if ((e.hash & bit) == 0) { //5.3.1、如果loTail为空,则代表当前节点为第一个节点 if ((e.prev = loTail) == null) //5.3.1.1、将loHead赋予为第一个节点 loHead = e;
//5.3.2、如果loTail不为空 else //5.3.2.1、将当前节点作为loTail的后一个节点 loTail.next = e;
//5.3.3、loTail为新增节点 loTail = e; //5.3.4、统计原索引位置相同元素的个数 ++lc; } //5.4、如果e的hash值和老表的容量进行与运算为1,则扩容后的索引位置为原索引位置+oldCap else { //5.4.1、如果hiTail为空,代表该节点为第一个节点 if ((e.prev = hiTail) == null) //5.4.1.1、则将hiHead复制为第一个节点 hiHead = e; //5.4.2、如果hiTail不为空 else //5.4.2.1、将当前节点作为hiTail的后一个节点 hiTail.next = e; //5.4.3、hiTail为新增节点 hiTail = e; //5.4.4、统计原索引位置+oldCap元素的个数 ++hc; } }
//5.5、如果原索引位置的节点不为空 if (loHead != null) { ///5.5.1、如果节点个数<=6个,则将红黑树树转为链表结构 if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map);
///5.5.2、如果节点个数>6个 else { //5.5.2.1、将原索引位置的节点设置为对应的头节点 tab[index] = loHead; //5.5.2.2、如果hiHead不为空,则代表原来的红黑树,老表的红黑树由于节点被分到两个位置,即已经被改变了,需要重新构建新的红黑树 if (hiHead != null) //5.5.2.2.1、构建红黑树 loHead.treeify(tab); } } //5.6、如果索引位置为原索引+oldCap节点不为空 if (hiHead != null) { //5.6.1、如果节点个数<=6个,则将红黑树树转为链表结构 if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map);
//5.6.2、如果节点个数>6个 else { //5.6.2.1、将索引位置为原索引+oldCap的节点设置为对应的头节点 tab[index + bit] = hiHead;
//5.6.2.2、如果loHead不为空,则代表原来的红黑树,老表的红黑树由于节点被分到两个位置,即已经被改变了,需要重新构建新的红黑树 if (loHead != null) //5.6.2.2.1、构建红黑树 hiHead.treeify(tab); } } } |
新增小结
1、根节点(root)指的是红黑树最上面的那个节点,也就是没有父节点的节点。
2、转为红黑树节点后,链表结构还存在,通过next属性维持着,红黑树节点在进行操作时,都会维护链表的结构,并不是转为红黑树节点,链表结构就不存在了。
3、在红黑树上,叶子节点也可能有next节点,因为红黑树结构跟链表结构是互不影响的,不会因为是叶子节点就说该节点已经没有next节点。
4、源码中的一些变量定义。
如果定义了一个节点p,则pl为p的左节点(p left);
pr(p right)为p的右节点;
pp(p parent)为p的父节点;
ph(p hash)为p的hash值;
pk(p key)为p的key值;
kc(key class)为key的类等;
另外源码中还很喜欢在if/for等语句赋值并判断。
5、源码中进行红黑树查找时,会反复使用两条规则。
一是如果目标节点的hash值小于p节点的hash值,则向p节点的左边遍历,否则就向p节点的右边遍历。
二是如果目标节点的key值小于p节点的key值,则向p节点的左边遍历,否则向右边遍历。
这两条规则都是利用了红黑树的特点(左节点 < 根节点 < 右节点)。
6、源码中进行红黑树查找时,会用dir(direction)来表示向左还是向右查找,dir存储的值是目标节点的hash/key与p节点的hash/key的比较结果。
7、根据key计算得到 key.hash = (h = k.hashCode()) ^ (h >>> 16);根据key.hash计算得到桶数组的索引 index = key.hash & (table.length - 1),就找到了key的存放位置。
如果这个位置没有数据,则用这个数据生产一个节点保存新数据,返回null;
如果该位置有数据是一个红黑树,那么自信相应的插入/更新操作;
如果该位置有数据是一个链表,分两种情况,一是该链表没有这个节点,采用尾插入法新增节点保存新数据,返回nul。另外是该链表上有这个节点,找到该节点,并更新新数据,返回老数据。判断依据是key.hash是否一样。
8、HashMap的put会返回key的上一次保存的数据
转载地址:http://dlmxi.baihongyu.com/