HashMap(源码分析)—慎入全是干货,没有水

目录

前言

准备工作

HashMap创建

1、传入容量和阈值系数

2、传入容量

3、默认初始化

4、直接传入Map

Put

1、扩容

 2、索引定位

1、Hash冲突

3、放入树节点PutTressVal()

1、find()

2、moveRootToFront()

4、插入链表尾部

5、转红黑树

      1、重新扩容

Get

Remove

1、removeTreeNode()

2、untreeify()         

总结


前言

        日常开发经常或使用HashMap来存储对象、数据或者传递参数,知道它的原理key不可重复,key、value可为null,就可以使用了,但实际为什么会有这些特性,以及Hash冲突它是如何解决,一概不知,所以就花了点时间看了看源码。

        这篇文章会从HashMap的创建、Put、索引定位、扩容、Hash冲突、链表转红黑树、Get、Remove这些方面,从源码获取答案。

准备工作

        main方法简单写几行代码。

public static void main(String[] args) {

     HashMap<String,Object> map = new HashMap<>();
     map.put("第一个","zhanghao");
     map.put("第二个","zhanghao2");
     map.putAll(map);
     map.remove("第一个");

}

HashMap创建

        HashMap创建的时候,如果像我上面这种创建方式,是不会赋予初始容量的。在放入第一个元素时候长度会赋予上。

        看下源码:(个人喜欢把loadFactor叫做阈值系数,因为:容量*阈值系数=阈值

    //1、传入容量和负载系数(我喜欢叫阈值系数,因为:容量*阈值系数=阈值)
    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);
    }

    //2、传入容量
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    //3、默认初始化,跟我创建方式一样
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    //4、直接传入Map
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

1、传入容量和阈值系数

        容量就是这个map的初始化长度,阈值就是map中当达到阈值长度时就出发扩容,比如:长度为16,阈值系数为0.75,则map的阈值就是12,那么map内的key数量达到了12,那就开始扩容。

        这个很简单,假如执行下面代码:

HashMap<String,Object> map = new HashMap<>(14,0.6f);

则,这个Map的初始容量就是4, 阈值系数为0.6,这里有个方法是tableSizeFor(initialCapacity)

    //它会返回一个“容量”的根号倍
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

这里的第一行则是取2的次方倍最近的数字,比如,传入13,则会传出15,看一下源码

    @HotSpotIntrinsicCandidate
    public static int numberOfLeadingZeros(int i) {
        // HD, Count leading 0's
        if (i <= 0)
            return i == 0 ? 32 : 0;
        int n = 31;//这里可以理解为2的次方数字
        if (i >= 1 << 16) { n -= 16; i >>>= 16; }
        if (i >= 1 <<  8) { n -=  8; i >>>=  8; }
        if (i >= 1 <<  4) { n -=  4; i >>>=  4; }
        //i = 13则会进入这个if判断,n次方减去2个次方,i无符号右移两位
        if (i >= 1 <<  2) { n -=  2; i >>>=  2; }
        //最后返回 29-1=28
        return n - (i >>> 1);
    }

最后int n = -1>>>28,-1右移28位变成15

 tableSizeFor()方法最后返回16。

        在这里初始化的阈值还不是真正的map阈值: 容量*阈值系数,真正的阈值是在放入第一个值时进行扩容时才会设置,再继续往后面看,原因写在“扩容”部分。

2、传入容量

        这里调用初始方法1,只不过用的是初始阈值系数0.75,这里就不再细说了。

3、默认初始化

        这里源码备注全部属性都是用默认的,也就是容量16,阈值系数0.75。

4、直接传入Map

        这里使用的还是默认的阈值系数0.75,下一行调用了一个方法putMapEntries(),看一下源码

    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();//获取map长度
        if (s > 0) {
            if (table == null) { // pre-size
                //刚开始传入map,table一定为空,所以第一次会进来
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            //这里的意思是,如果阈值设定后,长度还是大于阈值,那就直接-》扩容
            else if (s > threshold)
                resize();
            //循环把map的key-value放入新的map中
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

        这里涉及到了,扩容,还有Put操作,后面再看。

Put

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) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
//1、判断table是否为空或者length为0,如果是则扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
//2、通过hash值计算索引位置,称它为:索引定位,如果此处为空,则新增节点,这里可以看出HashMap的基础数据结构就是Node为元素的数组;(长度-1)和key的hash值做“&”操作可以定位,这里可以细看一下
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // table表该索引位置不为空,则进行查找
        Node<K,V> e; K k;
        // 判断p节点的key和hash值跟传入的key和hash是否相等,如果相等, 将p节点赋值给e节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
//3、判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法放置目标节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
            for (int binCount = 0; ; ++binCount) {
//4、如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
    //校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,因为第一次判断已经跳过了一个节点,所以这里减一
                    if (binCount >= TREEIFY_THRESHOLD - 1)
//5、转红黑树了,这里仔细看一下
                        treeifyBin(tab, hash);
                    break;
                }
                //如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  // 将p指向下一个节点
            }
        }
  //如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点原来的value,并返回value
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用于LinkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    //如果插入节点后节点数超过阈值,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);  // 用于LinkedHashMap
    return null;
}

这里按照代码块里面的顺序来看一下

1、扩容

        从刚开始学HashMap就知道的,扩容是以原来长度2倍扩容,也就是2的次方,来看一下源码

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //老表的容量不为0,即老表不为空
    if (oldCap > 0) {
        //判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,因为这时阈值*2已经远远大于Integer.MAX_VALUE,所以直接赋值为最大就完了,并直接返回老表,
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
//两倍扩容在这里:::::容量扩容两倍<最大容量并且容量>=16, 则将新阈值设置为原来的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //这里表示老表的容量为0, 老表的阈值大于0,
    //这种情况是刚初始化完,第一次扩容才会出现的,是因为初始化时阈值设置为容量了,容量没有设置
    //这里就把老阈值(初始化的容量)设置为新容量。
    else if (oldThr > 0)
        newCap = oldThr;
    else {
//这里表示老表的容量为0, 老表的阈值为0,这种情况是我上面“准备工作”中的方式创建的map,将阈值和容量设置为默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
//如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
//如果你使用 HashMap(int initialCapacity, float loadFactor)这种构造方式来初始化则就会进入这个if,这里才会设置真正的阈值,这就与上文对应上了
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //将新阈值设置为阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。
    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; 
   //如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
                if (e.next == null)
   //使用索引定位放置e节点
                    newTab[e.hash & (newCap - 1)] = e;
   //如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
   //如果是普通的链表节点,则进行普通的重hash分布
                    Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
                    Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
                    Node<K,V> next;
                    do {
                        next = e.next;
       //如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
      //如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e; 
                            else
                                hiTail.next = e;
                            hiTail = e; 
                        }
                    } while ((e = next) != null);
      //如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点
                    // 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
      //如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后
                    // 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    //返回新表
    return newTab;
}

//如果你使用 HashMap(int initialCapacity, float loadFactor)这种构造方式来初始化则就会进入上面代码块的这个if判断,这里才会设置真正的阈值,这就与上文对应上了
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }

 2、索引定位

        这里我平时一直记得是key是通过计算hash值然后在HashMap中存放,但是其实这只对了一半,具体原因看一下源码:

int index = (n - 1) & hash

        这里的hash就是key的hash值,怎么计算的这里不细说了,那么这里使用这样的位运算就是为了减小hash冲突,具体原因如下:

1、Hash冲突

        对于任意给定的对象,只要它的 hashCode() 返回值相同,那么计算得到的 hash 值总是相同的。我们首先想到的就是把 hash 值对 table 长度取模运算,这样一来,元素的分布相对来说是比较均匀的。

        但是模运算消耗还是比较大的,我们知道计算机比较快的运算为位运算,因此 JDK 团队对取模运算进行了优化,使用上面代码2的位与运算来代替模运算。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,这个优化是基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道 HashMap 底层数组的长度总是 2 的 n 次方,并且取模运算为 “h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是 HashMap 在速度上的优化,因为 & 比 % 具有更高的效率。

        在 JDK1.8 的实现中,还优化了高位运算的算法,将 hashCode 的高 16 位与 hashCode 进行异或运算,主要是为了在 table 的 length 较小的时候,让高位也参与运算,并且不会有太大的开销。

        下图是一个简单的例子:

        当 table 长度为 16 时,table.length - 1 = 15 ,用二进制来看,此时低 4 位全是 1,高 28 位全是 0,与 0 进行 & 运算必然为 0,因此此时 hashCode 与 “table.length - 1” 的 & 运算结果只取决于 hashCode 的低 4 位,在这种情况下,hashCode 的高 28 位就没有任何作用,并且由于 hash 结果只取决于 hashCode 的低 4 位,hash 冲突的概率也会增加。因此,在 JDK 1.8 中,将高位也参与计算,目的是为了降低 hash 冲突的概率。

 

3、放入树节点PutTressVal()

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;
    //将根节点赋值给p节点,开始进行查找
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        //传入的h小于此节点hash值,这向左边遍历
        if ((ph = p.hash) > h)
            dir = -1;
        //传入的h小于此节点hash值,这向右边遍历
        //这里的逻辑源自于红黑树的特性,左边<根节点<右节点
        else if (ph < h)
            dir = 1;
        //key和value都相等,则返回p节点
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        //如果k所属的类没有实现Comparable接口 或者 k和p节点的key相等
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
//1、第一次符合条件, 从p节点的左节点和右节点分别调用find方法进行查找, 如果查找到目标节点则返回
            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;
            }
            //否则使用定义的一套规则来比较k和p节点的key的大小, 用来决定向左还是向右查找
            dir = tieBreakOrder(k, pk); // dir<0则代表k<pk,则向p左边查找;反之亦然
        }
 //---------------------------------------------------------------------

        TreeNode<K,V> xp = p;   // xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值
        //dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            // 走进来代表已经找到x的位置,只需将x放到该位置即可
            Node<K,V> xpn = xp.next;    // xp的next节点
            //创建新的节点, 其中x的next节点为xpn, 即将x节点插入xp与xpn之间
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            //调整x、xp、xpn之间的属性关系
            if (dir <= 0)   // 如果时dir <= 0, 则代表x节点为xp的左节点
                xp.left = x;
            else        // 如果时dir> 0, 则代表x节点为xp的右节点
                xp.right = x;
            xp.next = x;    // 将xp的next节点设置为x
            x.parent = x.prev = xp; // 将x的parent和prev节点设置为xp
            //如果xpn不为空,则将xpn的prev节点设置为x节点,与上文的x节点的next节点对应
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
//2、进行红黑树的插入平衡调整
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}
1、find()

        去看这个find方法,就会发现遍历过程与此处的树遍历类似,就不细说了,这里看懂了,那么find就能看懂,这里看不懂,那就多看几遍画虚线往上的部分!

2、moveRootToFront()

        这个方法就是将root放到头节点的位置,如果当前索引位置的头节点不是root节点,,则将root的上一个节点和下一个节点进行关联, 将root放到头节点的位置, 原头节点放在root的next节点上,跟着这个思路看一下源码。

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    //看是不是空的,都是空的则为空谈,不用调整
    if (root != null && tab != null && (n = tab.length) > 0) {
        //索引定位
        int index = (n - 1) & root.hash;
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        //如果该索引位置的头节点不是root节点,则该索引位置的头节点替换为root节点
        if (root != first) {
            Node<K,V> rn;
            //将该索引位置的头节点赋值为root节点
            tab[index] = root;
            TreeNode<K,V> rp = root.prev;   // root节点的上一个节点
            //下面两个操作是移除root节点的过程
            //如果root节点的next节点不为空,则将root节点的next节点的prev属性设置为root节点的prev节点
            if ((rn = root.next) != null)
                ((TreeNode<K,V>)rn).prev = rp;
            //如果root节点的prev节点不为空,则将root节点的prev节点的next属性设置为root节点的next节点
            if (rp != null)
                rp.next = rn;
            //下面两个操作将first节点接到root节点后面
            //如果原头节点不为空, 则将原头节点的prev属性设置为root节点
            if (first != null)
                first.prev = root;
            //将root节点的next属性设置为原头节点
            root.next = first;
            //root此时已经被放到该位置的头节点位置,因此将prev属性设为空
            root.prev = null;
        }
        //检查树是否正常
        assert checkInvariants(root);
    }
}

4、插入链表尾部

        这里就是想强调一下,有个关键点,就是他这里判断了一下节点个数

 if (binCount >= TREEIFY_THRESHOLD - 1)

这里TREEIFY_THRESHOLD = 8;

因为进入方法就已经判断一个头节点了,所以这里减一

5、转红黑树

          这里直接进入方法看源码:

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //1、容量小于64, 调用resize方法进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    //继续索引定位,将该索引位置的节点赋值给e,从e开始遍历该索引位置的链表
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            //将链表节点转红黑树节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            //如果是第一次遍历,将头节点赋值给hd
            if (tl == null)	// tl为空代表为第一次循环
                hd = p;
            else {
                //如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性
                p.prev = tl;    // 当前节点的prev属性设为上一个节点
                tl.next = p;    // 上一个节点的next属性设置为当前节点
            }
            //将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)
            tl = p;
        } while ((e = e.next) != null);
        //将table该索引位置赋值为新转的TreeNode的头节点,如果该节点不为空,则以以头节点(hd)为根节点, 构建红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
1、重新扩容

                这里就引入了转红黑树的条件了,有时候在外面看只能看到节点数量大于8了,只看到了一半的条件,所以,转红黑树必须容量大于64节点数量大于8,才可以链表转红黑树哈!!

Get

        Get方法就是从Map中取key对应的value,例如:map.get("key");

    public V get(Object key) {
        Node<K,V> e;
        //调用getNode,又一次说明了,HashMap底层数据结构是node节点
        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) {
        //如果是树节点,就调用getTreeNode方法,进去看其实还是find()方法,还是那句话,把PutTreeVal()方法看懂了,红黑树遍历啥的不是事儿了奥
                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;
    }

Remove

        这个方法就是删除元素,例如::map.remove("key");

        这个过程就是先找到这个元素,再删除。

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

   final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
//--------------------------------------------------------------------------
//从这往上和get方法差不多,基本一模一样,就是先找出这个节点,那就不说了,继续看下面
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
//1、如果是树节点,则调用removeTreeNode()
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

1、removeTreeNode()

        看源码

/**
 * 红黑树的节点移除
 */
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    // --- 链表的处理start ---
    int n;
    // 1.table为空或者length为0直接返回
    if (tab == null || (n = tab.length) == 0)
        return;
    // 2.根据hash计算出索引的位置
    int index = (n - 1) & hash;
    // 3.将索引位置的头节点赋值给first和root
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    // 4.该方法被将要被移除的node(TreeNode)调用, 因此此方法的this为要被移除node节点,
    // 将node的next节点赋值给succ节点,prev节点赋值给pred节点
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    // 5.如果pred节点为空,则代表要被移除的node节点为头节点,
    // 则将table索引位置的值和first节点的值赋值为succ节点(node的next节点)即可
    if (pred == null)
        tab[index] = first = succ;
    else
        // 6.否则将pred节点的next属性设置为succ节点(node的next节点)
        pred.next = succ;
    // 7.如果succ节点不为空,则将succ的prev节点设置为pred, 与前面对应
    if (succ != null)
        succ.prev = pred;
    // 8.如果进行到此first节点为空,则代表该索引位置已经没有节点则直接返回
    if (first == null)
        return;
    // 9.如果root的父节点不为空, 则将root赋值为根节点
    if (root.parent != null)
        root = root.root();
    // 10.通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回
    // (转链表后就无需再进行下面的红黑树处理)
    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    // --- 链表的处理end ---
 
    // --- 以下代码为红黑树的处理 ---
    // 11.将p赋值为要被移除的node节点,pl赋值为p的左节点,pr赋值为p 的右节点
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
    // 12.如果p的左节点和右节点都不为空时
    if (pl != null && pr != null) {
        // 12.1 将s节点赋值为p的右节点
        TreeNode<K,V> s = pr, sl;
        // 12.2 向左一直查找,跳出循环时,s为没有左节点的节点
        while ((sl = s.left) != null)
            s = sl;
        // 12.3 交换p节点和s节点的颜色
        boolean c = s.red; s.red = p.red; p.red = c;
        TreeNode<K,V> sr = s.right; // s的右节点
        TreeNode<K,V> pp = p.parent;    // p的父节点
        // --- 第一次调整和第二次调整:将p节点和s节点进行了位置调换 ---
        // 12.4 第一次调整
        // 如果p节点的右节点即为s节点,则将p的父节点赋值为s,将s的右节点赋值为p
        if (s == pr) {
            p.parent = s;
            s.right = p;
        }
        else {
            // 将sp赋值为s的父节点
            TreeNode<K,V> sp = s.parent;
            // 将p的父节点赋值为sp
            if ((p.parent = sp) != null) {
                // 如果s节点为sp的左节点,则将sp的左节点赋值为p节点
                if (s == sp.left)
                    sp.left = p;
                // 否则s节点为sp的右节点,则将sp的右节点赋值为p节点
                else
                    sp.right = p;
            }
            // s的右节点赋值为p节点的右节点
            if ((s.right = pr) != null)
                // 如果pr不为空,则将pr的父节点赋值为s
                pr.parent = s;
        }
        // 12.5 第二次调整
        // 将p的左节点赋值为空,pl已经保存了该节点
        p.left = null;
        // 将p节点的右节点赋值为sr,如果sr不为空,则将sr的父节点赋值为p节点
        if ((p.right = sr) != null)
            sr.parent = p;
        // 将s节点的左节点赋值为pl,如果pl不为空,则将pl的父节点赋值为s节点
        if ((s.left = pl) != null)
            pl.parent = s;
        // 将s的父节点赋值为p的父节点pp
        // 如果pp为空,则p节点为root节点, 交换后s成为新的root节点
        if ((s.parent = pp) == null)
            root = s;
        // 如果p不为root节点, 并且p是pp的左节点,则将pp的左节点赋值为s节点
        else if (p == pp.left)
            pp.left = s;
        // 如果p不为root节点, 并且p是pp的右节点,则将pp的右节点赋值为s节点
        else
            pp.right = s;
        // 12.6 寻找replacement节点,用来替换掉p节点
        // 12.6.1 如果sr不为空,则replacement节点为sr,因为s没有左节点,所以使用s的右节点来替换p的位置
        if (sr != null)
            replacement = sr;
        // 12.6.1 如果sr为空,则s为叶子节点,replacement为p本身,只需要将p节点直接去除即可
        else
            replacement = p;
    }
    // 13.承接12点的判断,如果p的左节点不为空,右节点为空,replacement节点为p的左节点
    else if (pl != null)
        replacement = pl;
    // 14.如果p的右节点不为空,左节点为空,replacement节点为p的右节点
    else if (pr != null)
        replacement = pr;
    // 15.如果p的左右节点都为空, 即p为叶子节点, replacement节点为p节点本身
    else
        replacement = p;
    // 16.第三次调整:使用replacement节点替换掉p节点的位置,将p节点移除
    if (replacement != p) { // 如果p节点不是叶子节点
        // 16.1 将p节点的父节点赋值给replacement节点的父节点, 同时赋值给pp节点
        TreeNode<K,V> pp = replacement.parent = p.parent;
        // 16.2 如果p没有父节点, 即p为root节点,则将root节点赋值为replacement节点即可
        if (pp == null)
            root = replacement;
        // 16.3 如果p不是root节点, 并且p为pp的左节点,则将pp的左节点赋值为替换节点replacement
        else if (p == pp.left)
            pp.left = replacement;
        // 16.4 如果p不是root节点, 并且p为pp的右节点,则将pp的右节点赋值为替换节点replacement
        else
            pp.right = replacement;
        // 16.5 p节点的位置已经被完整的替换为replacement, 将p节点清空, 以便垃圾收集器回收
        p.left = p.right = p.parent = null;
    }
    // 17.如果p节点不为红色则进行红黑树删除平衡调整
    // (如果删除的节点是红色则不会破坏红黑树的平衡无需调整)
    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
 
    // 18.如果p节点为叶子节点, 则简单的将p节点去除即可
    if (replacement == p) {
        TreeNode<K,V> pp = p.parent;
        // 18.1 将p的parent属性设置为空
        p.parent = null;
        if (pp != null) {
            // 18.2 如果p节点为父节点的左节点,则将父节点的左节点赋值为空
            if (p == pp.left)
                pp.left = null;
            // 18.3 如果p节点为父节点的右节点, 则将父节点的右节点赋值为空
            else if (p == pp.right)
                pp.right = null;
        }
    }
    if (movable)
        // 19.将root节点移到索引位置的头节点
        moveRootToFront(tab, r);
}

removeTreeNode 图解

本图解忽略红黑树的颜色,请注意

下面的图解是代码中的最复杂的情况,即流程最长的那个,p 节点不为根节点,p 节点有左右节点,s 节点不为 pr 节点,s 节点有右节点。

另外,第一次调整和第二次调整的是本人根据代码而设定的,将第一次调整和第二次调整合起来看会更容易理解,如下:

  • 第一次调整 + 第二次调整:将 p 节点和 s 节点进行了位置调换,选出要替换掉 p 节点的 replacement
  • 第三次调整:将 replacement 节点覆盖掉 p 节点

2、untreeify()         

        在上面代码的第十点中,有个untreeify()方法,源码备注soo small,意思就是节点删除后节点个数小于6个,则会把红黑树又转为链表,源码就不看了,转红黑树会了这个自然没啥看头了。

总结

        我从创建初始化、Put、扩容、索引定位、Hash冲突、链表转红黑树,Get、Remove几个方面来解析,现在来对每个段落做个小结:

初始化:

  • 如果使用无参构造,则全部属性都为默认值,容量16,阈值12,阈值系数0.75;
  • 使用有参构造,则容量会是你传入容量参数最接近的2的次方数,例如:传入14,容量就会变成16,但是阈值系数会一直是你传入的参数,阈值也是按照容量*阈值系数来计算;

        这里还是强调一下,初始化完容量和阈值其实还没有赋上值,是在放入第一个元素之后才会定下来!!

扩容

  • 如果达到阈值,容量则会按照不大于容量最大值的情况,以2倍方式进行扩容,阈值是一样的逻辑;
  • 如果容量超过64,某个元素上的链表节点数超过8,则链表会转为红黑树;
  • 扩容时会触发hash重新分布,在Put()方法中的源码看到了,重 hash 分布后节点的新分布位置只可能有两个:“原索引位置” 或 “原索引+oldCap位置”。例如 capacity 为16,索引位置 5 的节点扩容后,只可能分布在新表 “索引位置5” 和 “索引位置21(5+16)”。

索引定位

        索引定位就是按照(Capacity-1)&hash,取位运算来进行。

Hash冲突

        这里HashMap采用了“索引定位”来减少Hash冲突,如果还是有则此节点后面添加节点变成链表的方式来解决Hash冲突。

Remove

        这个方法中,强调一点就是节点个数删除后小于6了,就会触发红黑树转链表的操作。

最后,HashMap是非线程安全的,线程安全可以使用ConcurrentHashMap代替。

这套代码看下来,确实不是很简单,可以看出JDK团队的技巧高超!

文中部分图片(hash冲突部分图文、移除树节点部分图文代码)参考来自(史上最详细的 JDK 1.8 HashMap 源码解析_程序员囧辉的博客-CSDN博客),感谢大佬!

最后我还整理汇总了⼀些 Java ⾯试相关的⾼质量 PDF 资料和免费Idea账号

公众号:Java小白,回复“⾯试” 和“idea破解”即可获取!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

派大星的无情铁锤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值