JDK核心类库源码 - HashMap

一、HashMap属性

先解释下这些属性的概念,有疑问请看思考。

// The default initial capacity - MUST be a power of two.
// 默认初始容量,必须是2的幂次方。即初始容量是2的四次方16【0 0001 左移四位 1 0000】
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// The maximum capacity, used if a higher value is implicitly specified by either of the constructors with arguments. 
// MUST be a power of two <= 1<<30.
// 哈希表的最大容量,即1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;

// The load factor used when none specified in constructor.
// 默认负载因子,这是HashMap里面比较重要的参数,当hash表中的元素数量达到容量的乘以负载因子的时候,哈希表会进行扩容操作。
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// The bin count threshold for using a tree rather than list for a bin. Bins are converted to trees when adding 
// an element to a bin with at least this many nodes. The value must be greater than 2 and should be at least 8 
// to mesh with assumptions in tree removal about conversion back to plain bins upon shrinkage.
// 树化阈值,当一个桶中的元素大于等于8的时候进行红黑树化
static final int TREEIFY_THRESHOLD = 8;

// The bin count threshold for untreeifying a (split) bin during a resize operation. Should be less than 
// TREEIFY_THRESHOLD, and at most 6 to mesh with shrinkage detection under removal.
// 扩容期间,红黑树降级为链表的阈值,当一个桶中的元素小于等于6的时候,树转化为链表
static final int UNTREEIFY_THRESHOLD = 6;

// The smallest table capacity for which bins may be treeified. (Otherwise the table is resized if too many nodes in a bin.) 
// Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts between resizing and treeification thresholds.
// 当桶的个数达到64的时候,才进行树化
static final int MIN_TREEIFY_CAPACITY = 64;

// The table, initialized on first use, and resized as necessary. When allocated, length is always a power of two. 
// (We also tolerate length zero in some operations to allow bootstrapping mechanics that are currently not needed.)
// 哈希表,HashMap 在构造函数中不会初始化 table 数组,而是在第一次调用 put 方法添加元素时才进行初始化。
transient Node<K,V>[] table;

// Holds cached entrySet(). Note that AbstractMap fields are used for keySet() and values().
// 在 HashMap 里,entrySet() 方法返回的是一个包含所有键值对(Map.Entry 对象)的集合。
transient Set<Map.Entry<K,V>> entrySet;

// The number of key-value mappings contained in this map.
// 元素数量
transient int size;

// The number of times this HashMap has been structurally modified Structural modifications are those that change the number 
// of mappings in the HashMap or otherwise modify its internal structure (e.g., rehash). This field is used to make iterators 
// on Collection-views of the HashMap fail-fast. (See ConcurrentModificationException).
// 记录其结构修改(改变映射数量也就是元素数量或内部结构如重新哈希)次数的字段,该字段用于让 HashMap 集合视图的迭代器具备快速失败机制,
// 以避免并发操作导致的迭代问题,若迭代中结构被修改则抛出 ConcurrentModificationException 异常。
transient int modCount;

// The next size value at which to resize (capacity * load factor).
// The javadoc description is true upon serialization.Additionally, if the table array has not been allocated, this field holds 
// the initial array capacity, or zero signifying DEFAULT_INITIAL_CAPACITY.
// 扩容的阈值(容量 * 负载因子),但是非常需要注意的是当hash数组尚未分配,这个字段保存的是初始数组容量,或者0,或者是默认初始容量
int threshold;

// The load factor for the hash table.
// 负载因子,可以通过构造函数赋值,如果没有指定,那么使用DEFAULT_LOAD_FACTOR 
final float loadFactor;

思考

  1. 为什么默认初始容量需要是2的幂次方?
    因为 HashMap 采用哈希表存储键值对,2的幂次方容量有助于提升哈希计算与冲突处理效率。

  2. 为什么使用位移计算?
    位运算比乘法运算效率更高,能加快数据处理速度。

  3. “元素数量”和“容量”有何区别?
    容量:指 HashMap 中桶(bucket)的数量,用于分配键值对存储位置,单个桶可容纳多个键值对,并可能触发红黑树化或链表降级。
    元素数量:指实际存储的键值对总数,由 Map.Entry 对象表示。

  4. 非扩容期间,UNTREEIFY_THRESHOLD 是否起作用?
    目前源码中该阈值在删除元素时可能触发红黑树降级为链表,具体逻辑待进一步分析。

  5. MIN_TREEIFY_CAPACITYTREEIFY_THRESHOLD 如何联动?
    HashMap 添加元素时,若桶内节点数达到 TREEIFY_THRESHOLD,会检查容量:若小于 MIN_TREEIFY_CAPACITY,则扩容缓解冲突;若大于等于 MIN_TREEIFY_CAPACITY,则将桶内链表转换为红黑树。

  6. Node<K,V>[] tableentrySet() 存储元素的区别?
    存储形式table 是基于哈希值索引的数组,通过链表或红黑树处理冲突;entrySet() 返回的集合抽象展示键值对,不涉及底层存储细节。
    用途table 用于 HashMap 内部的插入、查找、删除操作;entrySet() 便于外部遍历和数据处理。

  7. threshold 为何存在两种类型的值?
    HashMap 未初始化时,threshold 存储初始容量(必为2的幂次方):若构造函数指定容量为8,threshold 即为8;若指定9,则取最近的2的幂次方值16作为容量。

二、HashMap的构造函数

无参构造函数

loadFactor:赋值DEFAULT_LOAD_FACTOR
initcapacity:0
threshold:0

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

有参构造函数 - initialCapacity loadFactor

loadFactor:通过入参指定
initcapacity:通过入参指定
threshold:通过initcapacity计算得到threshold(返回大于等于指定容量的的最小的2的n次幂),这个threshold用于创建table的时候初始化容量

       public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0) // 指定容量只能大于等于0,否则抛异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) // 指定容量超过了最大容量,取最大容量计算threshold
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 负载因子小于等于0或者非float,抛异常
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor; // 负载因子赋值为入参指定
        this.threshold = tableSizeFor(initialCapacity); //返回得到>=cap的最近的2的幂次方
    }

想要看懂下面的代码,那么需要了解下原码、反码、补码的概念,如果懂的话,可以跳过这段。
原码:最高位为符号位:0表示正数、1表示负数;其余位:数值的绝对值
十进制+58位原码:0000 0101
十进制-58位原码:1000 0101
反码:正数的反码与原码相同,负数的反码取值逻辑为符号位不变,其余各位取反
十进制+58位反码:0000 0101
十进制-58位反码:1111 1010
补码:正数的补码和原码相同,负数的补码在反码基础上+1
十进制+58位补码:0000 0101
十进制-58位补码:1111 1011
计算机内部都是使用补码的,是因其能统一处理符号位和数值位,简化加减法运算(将减法变加法)就比如+5-5,其补码相加刚好为0000 0000,解决零表示不唯一问题,契合硬件特性,且可扩大负数表示范围。
其他扩展:
按位与&:对应位都为1,结果才为1,否则为0
按位或|:对应位有1,结果才为1,否则为0
按位非~:对应位为0,结果为1,否则为0
按位异或^:对应位不同,结果为1,否则为0
按位左移<<:整个位数向左移动n位,右边补0,相当于乘2
按位右移>>:整个位数向右移动n位,左边根据符号位补,正数补0,负数补1,相当于除以2
无符号按位右移>>>:整个位数向右移动n位,正数和负数的左边都用 0 来补位
11101 :低位向高位借1,借位后个位变为2,结果位1101
11001 :低位向高位借1,高位是0,继续向高位的高位借1,结果为1011

	// 这个方法是计算一个 32 位整数 i 二进制表示中前导零的数量。前导零指的是从二进制数的最高位开始连续的 0 的个数。类似于二分查找算法
	// 例如,二进制数 0000 0000 0000 0000 0000 0000 0000 1010 有 31 个前导零。
	public static int numberOfLeadingZeros(int i) {
		// 如果i<0,代表是赋值,最高位符号位就是1,那么前导零为0个,如果i=0,二进制都为0,所以前导零为32个
        if (i <= 0) 
            return i == 0 ? 32 : 0;
        // 如果i>0,那么前导零最多31,先将n设置为31
        int n = 31;
        // 1<<16也就是0000 0000 0000 0001 0000 0000 0000 0000,如果i比1<<16还大,那么说明前导零最多15个,
        // 也就是n = 31 - 16 = 15, 继续将i无符号右移 16 位,将高 16 位移到低 16 位,方便后续检查
        if (i >= 1 << 16) { n -= 16; i >>>= 16; } 
        // 同理 1<<8也就是0000 0001 0000 0000,如果此时的i比1<<8大,那么说明前导零最多7个,
        // 也就是n = 15 - 8 = 7,继续将i无符号右移 8 位,将高 8 位移到低 8 位,方便后续检查
        if (i >= 1 <<  8) { n -=  8; i >>>=  8; } 
        // 同理 1<<4也就是0001 0000,如果此时的i比1<<4大,那么说明前导零最多3个,
        // 也就是n = 7 - 4 = 3,继续将i无符号右移 4 位,将高 4 位移到低 4 位,方便后续检查
        if (i >= 1 <<  4) { n -=  4; i >>>=  4; } 
        // 同理 1<<2也就是0100,如果此时的i比1<<2大,那么说明前导零最多1个,
        // 也就是n = 3 - 2 = 1,继续将i无符号右移 2 位,将高 2 位移到低 2 位,方便后续检查
        if (i >= 1 <<  2) { n -=  2; i >>>=  2; }
        // 无符号右移1位,检查此时的最高位是0还是1,结果为0,i的前导零的个数即为n,如果是1,那么则为n-1
        return n - (i >>> 1); 
    }
    
    // int类型是32位,所以在计算机里面是使用32位的二进制存储的,负数使用反码存储的。
    static final int tableSizeFor(int cap) { 
    	// 为什么使用cap - 1,这是为了处理 cap 本身就是 2 的幂次方的情况。
    	// 如果 cap 已经是 2 的幂次方,减 1 后可以确保后续计算得到的结果仍然是 cap。 
    	// -1在计算机中的存储为1111 1111 1111 1111 1111 1111 1111 1111(补码),
    	// 假设计算出来的前导数为29,以 cap = 8 为例,cap - 1 = 7,
    	// 二进制表示为 0000 0000 0000 0000 0000 0000 0000 0111,其前导零数量为 29。
    	// 将 -1 无符号右移 29 位后得到的二进制数是 0000 0000 0000 0000 0000 0000 0000 0111
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); 
        // //根据 n 的值进行边界检查和调整,
        // 加 1 后变为 0000 0000 0000 0000 0000 0000 0000 1000,即十进制的 8,这正是我们期望的结果。
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    

有参构造函数 - initialCapacity

loadFactor:赋值DEFAULT_LOAD_FACTOR
initcapacity:通过入参指定
threshold:通过initcapacity计算得到threshold(返回大于等于指定容量的的最小的2的n次幂),这个threshold用于创建table的时候初始化容量

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

有参构造函数 - map

loadFactor:赋值DEFAULT_LOAD_FACTOR
initcapacity:先通过 float ft = ((float)s / loadFactor) + 1.0F 计算预分配的哈希表容量,再以 int t = ((ft < (float)MAXIMUM_CAPACITY)? (int)ft : MAXIMUM_CAPACITY) 确定最终预分配容量得到initcapacity。 后续会详细分析putMapEntries方法的。
threshold:最后用 threshold = tableSizeFor(t) 更新阈值 。

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

三、从putMapEntries开始

    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) {
            	//计算预分配的hash表的容量
                float ft = ((float)s / loadFactor) + 1.0F;
                //确定最终的预分配容量
                int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
                // 如果预分配的容量还小于初始容量(注意这里说的不是阈值,而是指初始容量,因为此时table为空)
                if (t > threshold) 
                	//通过tableSizeFor方法计算得到threshold(>=t的最小的2的n次幂)
                    threshold = tableSizeFor(t);
            } else {
            	//如果hashtable已经被创建了,那么我们需要考虑是否扩容,
            	// 扩容的条件是,元素数量超过了阈值,并且hash表的长度小于最大容量,如果都满足,那么我们需要进行扩容操作。
                while (s > threshold && table.length < MAXIMUM_CAPACITY) 
                	//进行扩容,后面会具体讲resize()的。
                    resize();
            }
            // 开始遍历,讲旧表中的元素一个一个重新放入到新表中,请注意hash值是不变的,只是重新position到新的表中。
            // 简单来说就是新的索引位置i = (n - 1) & hash。为什么呢?n是table的size,也就是2的幂次方,
            // n-1的二进制肯定全为1,比如说n = 8,也就是2的三次方,二进制表示1000,n-1 = 7, 其二进制表示0111,
            // 使用(n - 1) & hash)就可以保证元素一定会hash到数组中(数组的索引是0 到 n-1,把 hash 值的高位部分屏蔽掉,只保留低位部分,
            // 从而确保计算出的索引值在 0 到 n - 1 的范围之内,也就是哈希表数组的有效索引范围)。
            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);
            }
        }
    }

四、红黑树

按照二叉查找(排序)树BST -> 平衡二叉树AVL -> RBT红黑树的顺序简单介绍如下:

二叉搜索树(BST)

二叉搜索树(Binary Search Tree),也称为二叉排序树,具有以下特性:

  • 特性1:左子树上所有结点的值均小于或等于它的根结点的值。
  • 特性2:右子树上所有结点的值均大于或等于它的根结点的值。
  • 特性3:左、右子树也分别为二叉排序树。

完美的二叉搜索树示例如下:

然而,当二叉搜索树退化成链表时,会呈现为如下形态:

此时可以看到二叉搜索树的缺点,其最坏情况下的搜索复杂度为 O ( n ) O(n) O(n)。为了避免这种特殊情况的发生,引入了平衡二叉树(AVL)和红黑树(red-black tree)。

平衡二叉树(AVL)

平衡二叉树(AVL树)由两位俄罗斯数学家G.M. Adelson-Velsky和E.M. Landis提出,具有以下特性:

  • 特性1:对于任何一颗子树的root根结点而言,它的左子树任何节点的key一定比root小,而右子树任何节点的key 一定比root大。
  • 特性2:对于AVL树而言,其中任何子树仍然是AVL树。
  • 特性3:每个节点的左右子节点的高度之差的绝对值最多为1。

对这些特性的理解是:特性1表明AVL树继承于BST,而特性3是AVL树保持平衡的条件。一旦插入、删除操作破坏了平衡,AVL树会自动调整以保持平衡。

那么是如何保持平衡的呢?继续往下看
以下是AVL树四种失衡场景及自平衡的图文说明:

LR型失衡,只要经过一次左旋,就变成了LL失衡,处理就同LL失衡的自平衡操作了
RL型失衡,只要经过一次右旋,就变成了RR失衡,处理就同RR失衡的自平衡操作了
### 红黑树(RBT) 红黑树是一种自平衡的二叉查找树。它在每个节点上增加一个存储位来表示节点的颜色,颜色取值为红色或黑色。通过对从根到叶子的任意一条路径上各节点的着色方式加以限制,红黑树能够确保没有一条路径会比其他路径长出两倍,从而实现近似平衡的效果。

红黑树具有以下特性:

  1. 每个节点的颜色,非红即黑。
  2. 根节点的颜色必定为黑色。
  3. 每个叶子节点(NIL节点,即空节点)均为黑色。
  4. 若一个节点为红色,那么它的两个子节点都为黑色。
  5. 从任意一个节点出发,到该节点的所有后代叶子节点的所有路径上,所包含的黑色节点数目相同。

需重点关注:
NIL 节点特性:红黑树的叶子节点 NIL 为虚拟节点。当插入红色新节点时,该节点实际附带两个黑色 NIL 空节点,这是维持树结构规则的重要机制。
设计原理:红黑树采用空间换时间策略,通过为节点增设颜色属性,减少平衡操作次数,提升动态操作效率。
高度本质:若移除所有红色节点,会发现从根节点到任意叶子节点的路径高度一致。因此,红黑树的有效高度由黑色节点数量决定。

红黑树插入节点的规则
默认新插入的节点为红色,这是因为父节点为黑色的概率较大,新节点为红色可以避免颜色冲突。具体插入情况如下:

  1. 红黑树为空树:直接插入新节点,并将其颜色设为黑色。
  2. 插入节点的key已存在:直接更新节点值,节点颜色不变。
  3. 插入节点的父节点为黑色:直接插入新节点,不会影响树的平衡。
  4. 插入节点的父节点为红色且父节点的兄弟节点为红色
    • 变色处理:将(祖父节点、父节点、当前节点)的颜色组合由“黑红红”变为“红黑红”,同时将父亲和兄弟节点的颜色由红色变为黑色。
    • 自平衡处理:如果祖父节点的父节点是黑色,则无需再处理;否则将祖父节点设置为当前节点,继续插入操作并进行自平衡处理,直至树达到平衡状态。若祖父节点就是根节点,那么直接将祖父节点变为黑色即可。
  5. 插入节点的父节点为红色、父节点的兄弟节点为黑色(NIL节点)且父亲节点是祖父节点的左子节点(会存在LL和LR失衡问题)
    • 5.1 LL失衡问题(新插入节点为父节点的左子节点)
      • 变色处理:将颜色组合由“黑红红”变为“红黑红”。
      • 旋转操作:对祖父节点进行右旋操作(变色后,父亲节点所在路径会多一个黑色节点,破坏了平衡)。
    • 5.2 LR失衡问题(新插入节点为父节点的右子节点)
      • 旋转操作:对父节点进行左旋操作,使原来的父节点变为当前节点的左子节点。
      • 自平衡处理:与LL失衡情况相同,将新的左子节点设为当前节点,继续进行插入操作。
  6. 插入节点的父节点为红色、父节点的兄弟节点为黑色(NIL节点)且父亲节点是祖父节点的右子节点(会存在RL和RR失衡问题)
    • 6.1 RR失衡问题(新插入节点为父节点的右子节点)
      • 变色处理:将颜色组合由“黑红红”变为“红黑红”。
      • 旋转操作:对祖父节点进行左旋操作(变色后,父亲节点所在路径会多一个黑色节点,破坏了平衡)。
    • 6.2 RL型失衡问题(新插入节点为父节点的左子节点)
      • 旋转操作:对父节点进行右旋操作,使原来的父节点变为当前节点的右子节点。
      • 自平衡处理:与RR失衡情况相同,将新的右子节点设为当前节点,继续进行插入操作。

五、深入resize()

// 初始化表的大小或者使表的大小增倍,如果为null,则根据字段阈值中持有的初始容量目标进行分配。否则因为我们使用的是2的幂扩展,
// 所以每个bin中的元素必须保持相同的索引或者在新表中以2的幂偏移量移动。
final Node<K,V>[] resize() {
		// 赋值oldTab为当前表
        Node<K,V>[] oldTab = table;
        // 赋值oldCap为当前表的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 赋值oldThr为当前阈值
        int oldThr = threshold;
        // 初始化newCap和newThr为0
        int newCap, newThr = 0;
        if (oldCap > 0) {
        	// 如果旧表容量大于0且容量大于等于最大容量,直接将阈值调成最高阈值,返回旧表,不再扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 如果旧表容量大于0且新表容量(=旧表容量扩一倍)后小于最大容量且旧表容量大于等于默认初始容量,则将阈值扩大一倍
            // 为什么要oldCap >= DEFAULT_INITIAL_CAPACITY这个条件呢?
            // 当 oldCap >= DEFAULT_INITIAL_CAPACITY 时,说明哈希表已经有一定规模,此时进行翻倍扩容可以有效减少哈希冲突,提高性能。
            // 当 oldCap < DEFAULT_INITIAL_CAPACITY 时,使用默认初始容量和阈值可以避免前期过度占用内存,同时简化初始化过程。
            // 这种策略在保证性能的前提下,尽可能地平衡了内存使用和计算开销。
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; 
        }
        // 如果oldCap<=0,说明hash table还未创建,但是oldThr>0说明oldThr存储的是初始容量,新容量就等于这个初始容量
        else if (oldThr > 0) 
            newCap = oldThr;
        // 如果oldCap<=0,oldThr <=0 那么说明表也没创建,初始容量也没有指定,那么直接用默认的初始容量和默认的负载因子*默认初始容量作为阈值
        else {
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果新阈值还为0,(也就是oldCap>0的情况下某些条件不满足 || oldThr > 0) 那么基于新表容量和负载因子计算预分配的阈值。
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            // 如果新表容量小于最大容量且阈值小于最大容量,那么直接用预分配的阈值作为新阈值,否则用Integer的最大值2^31 - 1作为新阈值
            // 为什么不用newCap * loadFactor 呢?
            // 是为防止 newCap * loadFactor 结果超出 MAXIMUM_CAPACITY 导致的溢出问题,同时可表示哈希表已达最大容量的特殊状态
            // 并简化边界条件处理,提升代码稳定性与性能。
            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)
                        // 旧表中e的位置是通过(n - 1)&hash)计算索引保证索引范围处于哈希表数组的有效索引范围内,n表示数组容量,这里同样也是。
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                    	// 如果是红黑树,那么需要将红黑树的每个节点也重新position,是根据next来的,每个节点都会存入他的下一个节点
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                    	// 如果是链表,那么也是将旧表中的元素重新散列到新表中。使用头插法,jdk1.8之前的尾插法会存在死循环问题。
                    	// 为什么存在死循环问题呢?现存在链表A->B->C->D,假设存在线程1和线程2,线程1拿到A,下一个指向节点是B 此时暂停
                    	// 线程2同样拿到A,下一个节点指向B,将B插入到A的头部以后,再拿到C,最终完成插入,得到结果如下: D -> C -> B -> A
                    	// 此时线程2恢复工作,插入B以后,拿到B的下一个节点,发现又是A,那么就变成了B->A->.....->B->A
                    	// 不过根本原因还是HashMap不能在多线程环境下使用!!!
                    	// loHead 表示低位链表的头部节点 loHead 表示低位链表的尾部节点
                        Node<K,V> loHead = null, loTail = null;
                        // hiHead 表示低位链表的头部节点 hiTail表示低位链表的尾部节点
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 为什么要用e.hash & oldCap == 0进行判断呢?
                            // 假设oldCap的十进制为8,即2的3次方,二进制位为0000 1000,而oldCap-1的二进制位0000 0111
                            // 假设e的hash为1101 1111 ,那么结果为0000 1000
                            // 假设e的hash为1101 0111 ,那么结果为0000 0000
                            // 旧表中e的索引位置通过hash以后分布在0~111之间,
                            // 所以当e.hash & oldCap == 0也就意味着这个e还在低位,那么新索引在新表中还保持相同的索引,
                            // 如果e.hash & oldCap == 1,那么意味着这个e会被重新散列到高位,那么新索引在新表中以2的幂偏移量即oldCap移动。
                            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;//新索引在新表中以2的幂偏移量即oldCap移动
                        }
                    }
                }
            }
        }
        return newTab;
    }
		// 在 HashMap 扩容时对红黑树进行拆分操作。当 HashMap 扩容时,原有的红黑树节点需要重新分配到新的数组桶中,
		// 该方法会根据节点的哈希值和扩容时的偏移量(bit)将红黑树拆分成两个链表或红黑树,分别放置在新数组的不同位置。
        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // loHead 表示低位链表的头部节点 loHead 表示低位链表的尾部节点
            TreeNode<K,V> loHead = null, loTail = null;
            // hiHead 表示高位链表的头部节点 hiHead 表示高位链表的尾部节点
            TreeNode<K,V> hiHead = null, hiTail = null;
            // lc 低位链表的节点数量:用于判断是否需要树化treeify还是非树化untreeify
            // hc 高位链表的节点数量:用于判断是否需要树化treeify还是非树化untreeify
            int lc = 0, hc = 0;
            // 遍历当前红黑树的所有节点
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
            	// 保存下一个节点
                next = (TreeNode<K,V>)e.next;
                // 断开当前节点的 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,将红黑树转换为普通链表
                if (lc <= UNTREEIFY_THRESHOLD) 
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
            	// 如果高位链表的节点数量小于等于红黑树降级为链表的阈值6,将红黑树转换为普通链表
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

以上内容为自己阅读源码+结合网上经验所得,如有不对之处,望指出!
我会持续更新的,目前还只看了部分源码
时间:2025/05/05
JDK版本:17

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值