【Java】深入解析 Java 集合底层原理:HashMap 扩容与 TreeMap 红黑树实现

深入解析 Java 集合底层原理:HashMap 扩容与 TreeMap 红黑树实现


🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

  • [深入解析 Java 集合底层原理:HashMap 扩容与 TreeMap 红黑树实现](#深入解析 Java 集合底层原理:HashMap 扩容与 TreeMap 红黑树实现)
  • [一、HashMap 的扩容机制(JDK 8)](#一、HashMap 的扩容机制(JDK 8))
    • [1.1 扩容触发条件](#1.1 扩容触发条件)
    • [1.2 扩容完整流程](#1.2 扩容完整流程)
      • [1.2.1 核心源码解析(resize() 方法片段)](#1.2.1 核心源码解析(resize() 方法片段))
  • [二、TreeMap 的红黑树实现](#二、TreeMap 的红黑树实现)
    • [2.1 红黑树五大性质](#2.1 红黑树五大性质)
    • [2.2 插入节点后的平衡调整](#2.2 插入节点后的平衡调整)
      • [2.2.1 核心源码解析(fixAfterInsertion() 方法片段)](#2.2.1 核心源码解析(fixAfterInsertion() 方法片段))
      • [2.2.2 插入修复流程流程图(Mermaid)](#2.2.2 插入修复流程流程图(Mermaid))
  • [三、HashMap 与 TreeMap 的差异对比](#三、HashMap 与 TreeMap 的差异对比)
    • [3.1 性能差异](#3.1 性能差异)
    • [3.2 线程安全](#3.2 线程安全)
    • [3.3 使用场景](#3.3 使用场景)
  • 四、总结

Java 集合框架是开发中的核心工具,其中 HashMap 和 TreeMap 作为 Map 接口的两大常用实现,分别以哈希表和红黑树为底层数据结构,适配不同业务场景。本文将聚焦二者的核心底层机制------HashMap 的扩容流程与 TreeMap 的红黑树实现,从源码、流程、原理三个维度展开解析,帮助中级开发者夯实底层基础。

一、HashMap 的扩容机制(JDK 8)

HashMap 基于哈希表实现,核心优势是 O(1) 级别的查找与插入效率,但这依赖于合理的负载因子与容量设计。当 put 操作导致哈希表元素数量超过 容量×负载因子 时,会触发扩容(resize)机制,通过容量翻倍、rehash 迁移节点来保证操作效率。JDK 8 对 HashMap 进行了重大优化,引入红黑树解决链表过长问题,同时在扩容 rehash 时采用高位 bit 判断策略,避免全量重新计算哈希。

1.1 扩容触发条件

HashMap 的扩容触发时机主要在 put 操作后:

  • 当哈希表为空时,初始化容量为默认值 16(DEFAULT_INITIAL_CAPACITY),负载因子默认 0.75(DEFAULT_LOAD_FACTOR);

  • 当元素数量(size)超过 threshold = 容量×负载因子 时,触发扩容;

  • 当链表长度超过阈值 8(TREEIFY_THRESHOLD),且数组容量小于 64(MIN_TREEIFY_CAPACITY)时,不直接树化,而是先扩容。

1.2 扩容完整流程

扩容核心是 resize() 方法,流程分为三步:容量翻倍、rehash 迁移节点、更新阈值。其中 rehash 策略是 JDK 8 的优化重点,通过高位 bit 判断节点新位置,避免重复计算哈希。

1.2.1 核心源码解析(resize() 方法片段)

java 复制代码
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 若旧容量超过最大值(2^30),则阈值设为Integer.MAX_VALUE,不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 容量翻倍(左移1位),若翻倍后不超过最大值且旧容量>=16,阈值也翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // 初始化时,若指定了阈值,容量设为阈值(适配带参构造)
        newCap = oldThr;
    else { // 无参构造初始化,使用默认容量和阈值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 计算新阈值(针对旧容量<16或初始化场景)
    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;
                // 红黑树节点,按红黑树规则迁移
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 链表节点,按高位bit判断拆分到两个新链表
                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;
                        // 核心判断:通过哈希值与旧容量的与运算,判断高位bit是否为0
                        // 若为0,节点留在原位置(相对于新容量,位置不变)
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 若为1,节点迁移到 原位置+旧容量 的新位置
                        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;
}

源码关键逻辑说明:

  • e.hash & oldCap:旧容量是 2 的幂次方,该运算本质是判断哈希值的第 log2(oldCap) 位(高位)是否为 1。例如 oldCap=16(二进制 10000),则判断哈希值的第 4 位是否为 1,无需重新计算哈希,仅通过位运算即可确定节点新位置。

  • 链表拆分:将原链表拆分为两个链表(lo 链表:高位为 0,位置不变;hi 链表:高位为 1,位置=原位置+旧容量),避免节点顺序反转,同时提升迁移效率。

  • 红黑树迁移:通过 split() 方法将红黑树拆分为两个子树,若子树节点数小于 6,则退化为链表,保证数据结构合理性。

二、TreeMap 的红黑树实现

TreeMap 基于红黑树实现,核心特性是保证键的有序性(自然排序或自定义排序),所有操作(插入、删除、查找)的时间复杂度均为 O(log n),这依赖于红黑树的五大性质与插入后的平衡调整机制。红黑树是一种自平衡二叉搜索树(BST),通过颜色标记(红/黑)和旋转操作维持平衡,避免退化为链表。

2.1 红黑树五大性质

红黑树的平衡依赖以下五大性质,缺一不可,共同保障 O(log n) 复杂度:

  1. 每个节点要么是红色,要么是黑色;

  2. 根节点必须是黑色;

  3. 所有叶子节点(NIL 节点,空节点)都是黑色;

  4. 若一个节点是红色,则其两个子节点必须是黑色(父子不能同为红色);

  5. 从任意节点到其所有叶子节点的路径上,黑色节点的数量相同(黑高相等)。

2.2 插入节点后的平衡调整

TreeMap 的 put 操作本质是红黑树的插入操作:先按二叉搜索树规则插入节点(默认插入节点为红色,减少调整次数),再通过 fixAfterInsertion() 方法修复红黑树性质,核心是五种旋转与颜色调整情形。

2.2.1 核心源码解析(fixAfterInsertion() 方法片段)

java 复制代码
private void fixAfterInsertion(TreeNode<K,V> x) {
    x.color = RED; // 新插入节点设为红色,降低调整概率
    // 循环调整:直到父节点为黑色或到达根节点
    while (x != null && x != root && x.parent.color == RED) {
        // 父节点是祖父节点的左孩子
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            TreeNode<K,V> y = rightOf(parentOf(parentOf(x))); // 叔叔节点(祖父右孩子)
            // 情形1:叔叔节点为红色(父红、叔红、祖黑)
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK); // 父节点设为黑色
                setColor(y, BLACK); // 叔叔节点设为黑色
                setColor(parentOf(parentOf(x)), RED); // 祖父节点设为红色
                x = parentOf(parentOf(x)); // 以祖父为新节点继续调整
            } else {
                // 情形2:叔叔节点为黑色,且当前节点是父节点的右孩子(父红、叔黑、祖黑,x为右子)
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x); // 以父节点为新节点,左旋
                    rotateLeft(x);
                }
                // 情形3:叔叔节点为黑色,且当前节点是父节点的左孩子(父红、叔黑、祖黑,x为左子)
                setColor(parentOf(x), BLACK); // 父节点设为黑色
                setColor(parentOf(parentOf(x)), RED); // 祖父节点设为红色
                rotateRight(parentOf(parentOf(x))); // 右旋祖父节点
            }
        } else { // 父节点是祖父节点的右孩子(对称情形)
            TreeNode<K,V> y = leftOf(parentOf(parentOf(x))); // 叔叔节点(祖父左孩子)
            // 情形4:叔叔节点为红色(对称情形1)
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                // 情形5:叔叔节点为黑色,且当前节点是父节点的左孩子(对称情形2)
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                // 对称情形3
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK; // 确保根节点为黑色,修复性质2
}

源码关键逻辑说明:

  • 插入节点默认红色:因为红色节点不会改变路径黑高,仅可能违反"父子不能同为红色"的性质,调整成本低于插入黑色节点。

  • 五大调整情形:本质可分为两类------叔叔节点为红色(情形1、4),通过颜色翻转解决;叔叔节点为黑色(情形2、3、5),通过旋转+颜色调整解决,旋转分为左旋(rotateLeft)和右旋(rotateRight),目的是调整节点位置,维持黑高平衡。

  • 循环终止条件:当节点到达根节点,或父节点为黑色时,红黑树性质已修复,循环结束,最后强制根节点为黑色,确保性质2不被破坏。

2.2.2 插入修复流程流程图(Mermaid)















插入节点x,设为红色
x是根节点?
设x为黑色,调整结束
父节点p为红色?
父节点p是祖父g的左孩子?
获取叔叔节点y(g的右孩子)
获取叔叔节点y(g的左孩子)
y为红色?
p、y设为黑色,g设为红色
x=g,循环继续
x是p的右孩子?
x=p,左旋x
p设为黑色,g设为红色
右旋g,调整结束
y为红色?
p、y设为黑色,g设为红色
x=g,循环继续
x是p的左孩子?
x=p,右旋x
p设为黑色,g设为红色
左旋g,调整结束

三、HashMap 与 TreeMap 的差异对比

HashMap 和 TreeMap 作为 Map 接口的实现,在底层结构、性能、线程安全、使用场景上差异显著,需根据业务需求合理选择:

3.1 性能差异

  • HashMap:插入、查找、删除平均时间复杂度为 O(1),最坏情况(哈希冲突严重,链表未树化)为 O(n);扩容时存在 rehash 迁移节点开销,频繁扩容会降低性能,建议初始化时指定合理容量。

  • TreeMap:所有操作时间复杂度均为 O(log n),不受哈希冲突影响,性能稳定;插入时需进行红黑树平衡调整,开销略高于 HashMap 平均情况,但优于 HashMap 最坏情况。

3.2 线程安全

二者均为非线程安全集合:

  • HashMap:并发场景下可能出现死循环(JDK 7 及之前,链表头插法导致)、数据覆盖等问题,JDK 8 改为尾插法解决死循环,但仍存在数据安全问题。

  • TreeMap:并发场景下会出现数据错乱、红黑树结构破坏等问题。

解决方案:使用 Collections.synchronizedMap() 包装,或使用并发集合(如 ConcurrentHashMap,无并发 TreeMap 实现,可通过 ConcurrentSkipListMap 替代,基于跳表实现有序并发)。

3.3 使用场景

  • HashMap:适用于无需保证键有序、追求高效读写的场景,如缓存存储、快速查询数据等,是日常开发中最常用的 Map 实现。

  • TreeMap:适用于需要键有序(自然排序/自定义排序)的场景,如按键排序遍历、获取键的首尾元素、范围查询(如 subMap() 方法)等,适合对有序性有强需求的业务。

四、总结

HashMap 以哈希表为核心,通过扩容机制和红黑树优化解决哈希冲突,追求极致读写效率;TreeMap 以红黑树为基础,通过平衡调整维持键的有序性,保证稳定的 O(log n) 性能。二者的设计思路体现了"效率优先"与"有序优先"的不同取舍,中级开发者需深入理解其底层原理,结合业务场景中的性能、有序性需求,选择最合适的集合实现,同时规避非线程安全带来的风险。

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

📕个人领域 :Linux/C++/java/AI

🚀 个人主页有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
源代码•宸2 小时前
Leetcode—3314. 构造最小位运算数组 I【简单】
开发语言·后端·算法·leetcode·面试·golang·位运算
June bug2 小时前
【配环境】安装配置Oracle JDK
java·数据库·oracle
FJW0208142 小时前
Python深浅拷贝
开发语言·python
Coder个人博客2 小时前
1233434235
java·开发语言
徐同保2 小时前
开发onlyoffice插件,功能是选择文本后立即通知父页面
开发语言·前端·javascript
Never_Satisfied2 小时前
C#数组去重方法总结
开发语言·c#
阿蒙Amon2 小时前
C#每日面试题-静态构造函数和普通构造函数区别
java·开发语言·c#
Java程序员威哥2 小时前
SpringBoot4.0+JDK25+GraalVM:云原生Java的性能革命与落地指南
java·开发语言·后端·python·云原生·c#
青小莫2 小时前
C++之模板
android·java·c++