透过源码看本质——1、HashMap

概述

HashMap 作为平时开发过程中常见的数据结构经常被用到,网上关于它的博文已经有非常多,为了加深我对它的理解,我计划就 JDK1.8 版本,通过源码的形式整理下它的原理。


HashMap

HashMap在源码中是这样定义的:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable,Serializable

这里我通过简单类图描述一下这几个类之间的关系:
在这里插入图片描述

  • Map 接口主要声明一些常用的 key-value 接口方法

  • AbstractMap 抽象类实现 Map 接口,它实现了部分接口方法,以及创建静态内部类 SimpleEntry

  • Cloneable 接口主要和 clone() 方法有关,实现该接口可以重写 clone() 方法

  • Serializable 主要和序列化有关,实现该接口可以让对象序列换

关于 SimpleEntry 类这里我们先不详细说明,等后面用到了我们再说


构造方法

HashMap 类的构造方法主要有以下几种:

static final float DEFAULT_LOAD_FACTOR = 0.75f;

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

其中前三种构造方法比较常见,第四种构造方法用的比较少,这里我们主要以常用的前三种为主。

通过源码我们可以看出,前三种构造方法主要是为了初始化属性 loadFactorthreshold 的值。这里我直接给出结论:

  • loadFactor:hashMap 的负载因子,当集合中元素个数达到负载因子所对应数量后 hashMap 会 扩容,通过源码可以看出默认的负载因子是0.75

  • threshold:hashMap 所对应管道数,每个管道可以保存多个元素

下面我们看一下 threshold 属性是如何计算出来的,即 tableSizeFor() 方法的源码:

static final int MAXIMUM_CAPACITY = 1 << 30;

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;
}

通过该方法可以获取大于等于参数的最小的2的幂次方:也就是说,如果参数是5,输出8,参数是9,输出16,每次都输出大于当前参数的最小二次幂。

从这里也就可以看出,hashMap 的管道个数总是2的幂次方,关于这样做的原因后面根据源码具体分析。下面我们看一个草图,通过这个草图对 hashMap 有一个大致的认识:
hashMap工作原理


put() 方法

有了上面的铺垫,下面我们具体看一下 hashmap 是如何保存一个元素的:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict){
	// 暂时先省略,下面着重解决
}

这里我先给出 putVal() 方法这五个参数所代表的意义:

  • hash:key 的 hash 值
  • key :要保存的 key
  • value:要保存的 value
  • onlyIfAbsent:是否修改已经存在的数据,如果该参数为 true,则不会修改已经存在的key
  • evict:该参数在 hashMap 中没有用到,在 hashMap 的子类 linkedHashMap 有用到,如果该参数为true,在添加元素后可能会删除顶部元素。

接下来我们看一个 hash() 方法的实现:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

该方法只是将 key 的 hashCode() 值散列的高位向地位移动一下。


Node内部类

在正式开始阅读 putVal() 源码前,我们先看看内部类 Node 的源码,该类是 putVal() 方法的基础:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
	
	// 省略 get() set()
    
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    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;
    }
}

通过源码我们可以看出,Node 内部保存 key、value 、hash 值以及指向下一个节点的 next 引用。其中它重写了 hashCode() 方法 和 equals() 方法。关于为啥重写这两个方法可以点击这里参考我之前的博客。

其实看到这里 hashMap 的原理基本上已经可以猜出来了,只需要把上面的草图改成下面这样:

HashMap原理
也就是说,hashMap 实际上就是通过 Node 内部类组成的数组和链表实现的


putVal() 源码

有了上面这些基础,我们具体来看 putVal() 方法具体是怎么做的。在下面源码中我尽量通过注释的形式介绍,部分特别重要的内容附加在源码后面:

// transient 表示该属性不会序列化,这里table就表示上图中的数组块内容
transient Node<K,V>[] table;

// 单个链表的阈值
static final int TREEIFY_THRESHOLD = 8;

// 数组中工作节点个数
transient int modCount;

// 当前hashMap存储的节点个数
transient int size;

// hashMap 所能容纳的最大节点个数
int threshold;

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 说明数组还没有初始化,即 hashmap 第一次添加元素时
    if ((tab = table) == null || (n = tab.length) == 0)
    	// resize()方法初始化数组,关于该方法的源码下面我会给出,这里的n表示数组的长度,可以理解为hashMap的管道数
        n = (tab = resize()).length;
    // 判断管道头是否为空,如果为空,直接将要put的元素添加到数组对应下标
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 判断数组元素key是否等于要put的元素key
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 判断是否使用TreeNode
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        	// 从链表头依次判断是否存在节点key与参数相等
            for (int binCount = 0; ; ++binCount) {
            	// 已经遍历到链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果某个链表的节点个数达到阈值
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                    	// 
                        treeifyBin(tab, hash);
                    break;
                }
                // 判断是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) {
        	// 这里的 e可能是新添加的Node节点,也可能是老的Node节点(hash相等,key相等情况)
            V oldValue = e.value;
            // 更新新值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 该方法在hashMap为空,什么都没做
            afterNodeAccess(e);
            // 返回给节点对应的老值
            return oldValue;
        }
    }
    // 工作节点个数加一
    ++modCount;
    // 判断hashmap元素数是否大于阈值
    if (++size > threshold)
    	// 扩容
        resize();
    // 该方法在 hashmap 为空
    afterNodeInsertion(evict);
    return null;
}

上述代码中,我主要提一下这行代码:

p = tab[(n - 1) & hash]

从这里我们可以看出,元素属于哪个数组节点是由它的hash值和数组长度决定的。元素的hash值是随机的,数组的长度是确定的,为了让元素尽可能平均的分配到所有节点,(n-1) & hash 的计算结果必须尽可能的均匀。

当元素的长度总是2的幂次方时,n-1的值转化为二进制总是111…1。在这种情况下,随机的hash值计算出的结果也就相对比较均匀,这也是为什么 hashmap 中数组的长度总是2的幂次方的主要原因。

  • 为什么要让元素分配的相对均匀呢?

    元素在数组上分配均匀无论是添加还是修改或是查询,都只需要遍历较少的节点数量,这对于效率的提升至关重要。


hashmap 的初始化及扩容

在整理 putval() 方法时,我们提到 hashmap 是在第一次put元素时初始化,当元素数量超过阈值时,扩容也是调用该方法,下面我整理一下 resize() 方法的源码:


static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 原先的数组大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 原先hashmap所能容纳的最大元素数
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    	// 数组已经达到最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
        	// 将最大元素数设置为 MAX
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 数组长度扩容为原来的两倍,如果hashmap最大容量超过16,也变为原来的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    // 如果数组长度为空,最大元素数不为0,将数组长度设置为容量大小
    else if (oldThr > 0)
        newCap = oldThr;
   	// hashmap还未初始化时,此时容量以及数组大小都为0
    else { 
    	// 默认数组大小为16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 默认hashmap阈值为 16 * 0.75(负载因子)
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果此时hashmap容量为0
    if (newThr == 0) {
    	// 重新计算最大容量
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 如果是数组扩容,重新分配所有元素
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 数组节点只有一个元素时的情况
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
              	// 存的是TreeNode时的情况
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 循环重新计算所有节点属于哪个下标
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

上述代码中,我主要提一下 hashmap 扩容时元素重新分配的原理:

  • 如果某个数组节点对应的链表只有一个元素,直接根据新的数组长度计算

  • 如果某个数组节点对应的链表有多个元素,则会根据一个 奇妙的算法计算

首先,hashmap 每次扩容时,数组长度都会变为原来的 2倍。转化为二进制就可以这样表示:
数组长度二进制
而计算元素属于哪个数组下标是和数组长度减1来计算的,数组长度减1转二进制分别对应:

  • 原来的数组长度转二进制:N 个 “1”
  • 现在的数组长度转二进制:N + 1 个 “1”

也就是说,通过新数组长度所计算出的下标,其实就是多算了一位第 n+1 位的 “1”,举个例子:

hash值等于10的元素转二进制 -> 1010,将它保存到长度为2的数组
此时属于哪个下标 -> 1010 & 0001,也就是说只看最后一位
如果数组长度扩容为原来的二倍
此时属于哪个小标 -> 1010 & 0011,也就是说,在原来的基础上,往前多判断一位
写成公式:扩容后的下标 = 扩容前下标 + 新加位是否为1
为了判断这个新加位是否为1,只需要让hash值直接和原数组长度进行&运算即可,
也就是上面代码所对应的 e.hash & oldCap
这个新加长度所对应的值,实际上也就是 oldCap

总结一下,数组扩容后,当前元素要么属于原来的数组下标,要么属于原来的数组下标加上原数组长度


Node 转 TreeNode

在整理 putVal() 方法时,当单个链表元素数量超过链表阈值时会调用 treeifyBin() 方法,在正式学习treeifyBin() 方法源码前,我们先看看内部类 TreeNode 的结构:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
	
	// 省略部分实现好的内部方法
}

从该对象的属性很明显可以看出,它是一种树形结果,并且是性能较好的 红黑树。而 LinkedHashMap.Entry 又是 Node 的子类,也就是说,该TreeNode 也是 Node 的子类,通过这种方式实现了 Node 转 TreeNode 的可能性,因为对象可以 向上转型


下面我们具体看 treeifyBin() 方法的源码:

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 {
        	// Node 转 TreeNode
            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);
    }
}

该方法是在某个链表超过阈值时,将所有Node节点转换为TreeNode节点,并通过前驱、后缀引用连接起来,下面我们主要看一下 treeify() 方法的源码:

final void treeify(Node<K,V>[] tab) {
	// 创建头节点
    TreeNode<K,V> root = null;
    // 循环当前链表节点
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        // 初始化头结点
        if (root == null) {
            x.parent = null;
            // 表明当前节点是“黑节点”
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            // 根据 hash 值向左向右遍历
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                // 如果hash值相等,根据comparable接口方法判断是否相等
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
				// 记录当前节点,表示要判断节点的父节点
                TreeNode<K,V> xp = p;
                // 只有进入该方法,才表示找的真正的落点,进行保存
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    // 重新让树表的平衡
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    // 确定数组对应节点是红黑树的头节点
    moveRootToFront(tab, root);
}

该方法表示当某个链表长度超过阈值时,将链表转换为红黑树的结构,提高效率。关于红黑树的结构我们后面做专门介绍(内容实在太多),就不再这里深入方法本身做探讨了。


get() 方法

看过put()方法的源码, get() 方法就简单了很多,下面我们直接看源码:

public V get(Object key) {
   Node<K,V> e;
   return (e = getNode(hash(key), key)) == null ? null : e.value;
}

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) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

看过 put() 方法源码后,看 get() 方法简直不要太轻松,几乎没有什么新内容,这里我们简单描述一下如果链表结构为 TreeNode 红黑树时,如何遍历。具体我们看代码:

final TreeNode<K,V> getTreeNode(int h, Object k) {
	// 这里主要保证每次遍历总是从最高级节点开始,root() 方法只是循环遍历到父节点
    return ((parent != null) ? root() : this).find(h, k, null);
}

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;
        // 向左遍历
        if ((ph = p.hash) > h)
            p = pl;
       	// 向右遍历
        else if (ph < h)
            p = pr;
        // hash 相等且 key 相等,说明就是当前节点
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        // hash 相等,但是左节点为空时,向右遍历
        else if (pl == null)
            p = pr;
       	// hash 相等,但是右节点为空时,向左遍历
        else if (pr == null)
            p = pl;
        // kc为 true 时,根据 compare 方法判断,默认为false
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            p = (dir < 0) ? pl : pr;
        // 根据左节点查询
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        // 根据右节点查询
        else
            p = pl;
    } while (p != null);
    return null;
}

总结一下:当链表转换为红黑树结构后,通过hash值作为参考,key值作为一锤定音的判断来实现的。当hash值相等时,根据对象的 compare() 方法做判断


putTreeVal() 方法

有了上面红黑树的基础,我们再来看看,当链表结构转化为红黑树结构后,如何添加新元素。具体我们看源码:

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;
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            // compare() 方法相同只会遍历一次
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                // 向左查,向右查,有值就返回
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            // 通过 compareTo 方法和 identityHashCode() 确定方向
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            // 将重新平衡后的红黑树的头结点设置在数组中
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

从源码可以看出,当链表转换为红黑树后,添加元素的逻辑实际上和转化时大致相同。


红黑树的扩容

上面我们讲了链表的扩容方法,这里我们主要看看当链表转换为红黑树后,整个红黑树的扩容方法,即 split() 方法的源码:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    // 遍历整个红黑树,将整个红黑树改为两个链表,一个链表记录新增位 & 为0,表示扩容后还在当前下标,另一个记录新增位 & 为1,表示扩容后再原下标+老数组长度下标
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
	// 处理对应数组下标不会变的情况
    if (loHead != null) {
    	// 如果长度小于6,TreeNode 转为 Node
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
            	// 红黑树化
                loHead.treeify(tab);
        }
    }
    // 处理数组下标变为原下标加原数组长度的情况,具体逻辑同上
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

总结以下:红黑树的扩容就是遍历整个红黑树,将结果转为两个链表,一个链表记录下标会变的情况,另一个链表记录下标不会变的情况,根据链表的长度,决定是否将链表转Node链表,还是红黑树化。


红黑树的平衡

在链表处理转红黑树或给红黑树添加新元素后,都会执行 balanceInsertion() 方法平衡红黑树,下面我们来看一下它的源码:

// root 表示红黑树的根节点,x表示最后一个添加的节点
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 就是root节点,直接返回
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        // 如果x的父节点就是黑色,并且它没有父节点,直接返回root,表示只有两个节点,root就是根节点
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // 如果x的父节点是它自身父节点的左子树
        if (xp == (xppl = xpp.left)) {
        	// x 的父节点的父节点的右子树不为空
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            // x 的父节点的父节点的右子树为空
            else {
            	// 判断 x 是否它父节点的右子树
                if (x == xp.right) {
                	// 向左旋转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);
                    }
                }
            }
        }
    }
}

上述代码本身比较复杂,主要涉及红黑树的左旋、右旋以及颜色的处理。关于这块我暂时也看的迷迷糊糊,等后续整理红黑树时着重整理,我暂时先给出两个关于左旋以及右旋的示意图:

在这里插入图片描述
如上图所示,就可以让树变得相对比较平衡。关于红黑树的只是暂且整理到这里,后面加上颜色着重分析。


关于 hashmap 的源码我就整理到这里,对于一些其他方法的实现,我想如果你读懂了 put() 以及 get() 方法,那基本不会有啥难度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值