深入解析 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) 复杂度:
-
每个节点要么是红色,要么是黑色;
-
根节点必须是黑色;
-
所有叶子节点(NIL 节点,空节点)都是黑色;
-
若一个节点是红色,则其两个子节点必须是黑色(父子不能同为红色);
-
从任意节点到其所有叶子节点的路径上,黑色节点的数量相同(黑高相等)。
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
💬 座右铭 : "向光而行,沐光而生。"
