概述
在 Java 集合框架中,TreeMap 作为 Map 接口的排序实现,基于红黑树数据结构保证了键值对在自然顺序或自定义比较器下的严格有序。它不仅提供了 O(log n) 时间复杂度的增删改查操作,还通过实现 NavigableMap 接口,赋予了开发者强大的导航方法(如 lowerKey、ceilingKey)与范围视图(如 subMap、headMap)能力,是“能排序的映射”的不二选择。本文将从红黑树的自平衡原理出发,结合 JDK 8 源码,全面解读 TreeMap 的存储结构、核心操作、导航特性及并发陷阱,带您彻底吃透这块硬骨头。
- 红黑树的自平衡特性:底层为红黑树结构,通过变色与旋转维持黑平衡,确保增删查操作的时间复杂度稳定在 O(log n)。
- 两种排序模式:自然排序(Key 实现 Comparable)与比较器排序(构造时传入 Comparator),Comparator 优先级高于 Comparable,Key 不可为 null(需比较)。
- NavigableMap 的导航与范围视图:提供 lowerKey/floorKey/ceilingKey/higherKey 等导航方法,以及 subMap/headMap/tailMap 等范围视图,支持高效的范围查找和遍历。
- 基于 compare 的统一判重:去重不依赖 hashCode/equals,而是通过 compareTo 或 compare 返回 0 判定 Key 相等,需保持比较器与 equals 的一致性。
- 非线程安全与 fail-fast:所有结构修改未同步,迭代器通过 modCount 实现快速失败,并发场景需使用 ConcurrentSkipListMap。
flowchart TD
subgraph Part1[基础认知篇]
A1[定义与核心特性] --> A2[接口与继承体系]
end
subgraph Part2[存储与构造篇]
B1[存储结构与核心字段] --> B2[构造方法]
end
subgraph Part3[核心原理篇]
C1[put操作与红黑树修复] --> C2[remove操作与平衡修复] --> C3[get与导航方法]
end
subgraph Part4[导航与子视图篇]
D1[NavigableMap导航方法全景] --> D2[子映射视图subMap/headMap/tailMap]
end
subgraph Part5[对比与陷阱篇]
E1[TreeMap vs HashMap vs LinkedHashMap] --> E2[TreeMap vs ConcurrentSkipListMap] --> E3[常见陷阱与最佳实践]
end
subgraph Part6[总结与面试篇]
F1[性能与注意事项] --> F2[面试高频专题]
end
Part1 --> Part2 --> Part3 --> Part4 --> Part5 --> Part6
图表说明:
- 第一层:全文六大篇章宏观布局
上述流程图以 subgraph 清晰划分了本文的六大核心篇章,从基础认知逐步深入到源码原理,再扩展至实战对比与面试总结,形成一条由浅入深、由用到底的学习路径。 - 第二层:篇章内模块递进
每个篇章内部的模块按学习依赖关系串联。例如 Part 3 核心原理篇,先讲put插入与自平衡修复,再讲remove删除与修复,最后讲get与导航,符合“插入→删除→查询”的认知链条。 - 第三层:数据结构映射
红黑树这一核心数据结构贯穿始终,在 Part 2 引入节点结构,Part 3 剖析其旋转变色,Part 4 体现其在范围查询中的高效性。 - 源码方法对应
篇章中的操作直接对应 JDK 8 中的核心方法,如put→put、fixAfterInsertion,remove→deleteEntry、fixAfterDeletion,导航方法 →getLowerEntry等,图表旨在引导读者提前建立方法映射。 - 关键结论强调
TreeMap 的核心价值在于“有序”和“导航”,图表结构从基础、存储、原理、导航,到对比陷阱,最终服务于对这两大特性的深刻掌握。
Part 1:基础认知篇
模块 1:定义、核心特性与适用场景
TreeMap 的定义可概括为:基于红黑树实现的、按键自然顺序或指定比较器排序的、线程不安全的 NavigableMap。它保证了遍历时键值对按照排序顺序输出。
核心特性列表:
- 键有序:所有键按比较器或自然顺序排列,迭代顺序一致。
- O(log n) 性能:得益于红黑树的高度平衡,
put、get、remove和导航操作的时间复杂度均维持在 O(log n)。 - 导航与范围视图:提供
NavigableMap接口的丰富方法,可快速找到lowerKey、floorKey等,并支持subMap、tailMap等子映射视图。 - 基于比较去重:判断键“相等”依靠
compareTo或compare方法返回 0,而非equals或hashCode。 - 不允许 null 键(取决于比较器):当依赖自然排序时,Key 必须实现
Comparable且不能为 null(比较时会 NPE);当提供可处理 null 的比较器时,则允许 null 键。 - 非线程安全:未做任何同步处理,多线程并发修改可能导致数据破坏或
ConcurrentModificationException。 - fail-fast:迭代器在迭代过程中若检测到结构被修改(
modCount变化),会立即抛出ConcurrentModificationException。
适用场景:
- 需要按键排序的键值存储,如用户 ID 排序的输出。
- 范围查询:例如“成绩在 80~90 分之间的所有学生”。
- 排行榜:天然排序支持首位/末位获取。
- 按序输出:任何需要遍历时有序的场景。
反例场景:
- 仅需快速存取,不关心顺序 → 使用 HashMap。
- 需保持插入顺序 → 使用 LinkedHashMap。
- 高并发有序映射 → 使用 ConcurrentSkipListMap。
flowchart TD
Start((开始选Map)) --> NeedOrder{需要键有序?}
NeedOrder -->|否| FastAccess{仅快速存取?}
FastAccess -->|是| HashMapChoice[选择HashMap]
FastAccess -->|需插入顺序| LinkedHashMapChoice[选择LinkedHashMap]
NeedOrder -->|是| Concurrency{是否高并发?}
Concurrency -->|是| SkipListChoice[选择ConcurrentSkipListMap]
Concurrency -->|否| TreeMapChoice[选择TreeMap]
TreeMapChoice --> Comp{Key自然可比较或提供Comparator?}
Comp -->|是| UseOK[安全使用]
Comp -->|否| Error[ClassCastException]
图表说明:
- 第一层:决策起点为“是否需要键有序”
该决策树将 键有序 作为第一分水岭。TreeMap 和 ConcurrentSkipListMap 均为有序实现,而 HashMap、LinkedHashMap 为无序或仅维护插入顺序。 - 第二层:并发与比较器约束
在有序分支中,进一步判断并发需求,高并发直接引导至ConcurrentSkipListMap;否则选用TreeMap。最终还要确保 Key 具有可比较性,否则会抛出 ClassCastException。 - 关键结论强调
当且仅当需要单线程环境下的有序映射时,TreeMap 才是最佳选择。若忽略并发,用 TreeMap 替代 ConcurrentSkipListMap 将导致线程安全问题;若忽略顺序,用 HashMap 更能提高效率。
模块 2:接口与继承体系
TreeMap 继承了 AbstractMap,实现了 NavigableMap 接口(该接口又继承自 SortedMap),同时支持 Cloneable 和 Serializable。这一继承链赋予了它排序、导航、克隆和序列化的能力。
classDiagram
class Map~K,V~ {
<<interface>>
}
class SortedMap~K,V~ {
<<interface>>
+comparator() Comparator
+firstKey() K
+lastKey() K
+headMap(K toKey) SortedMap
+tailMap(K fromKey) SortedMap
+subMap(K fromKey, K toKey) SortedMap
}
class NavigableMap~K,V~ {
<<interface>>
+lowerKey(K key) K
+floorKey(K key) K
+ceilingKey(K key) K
+higherKey(K key) K
+pollFirstEntry() Map.Entry
+pollLastEntry() Map.Entry
+descendingMap() NavigableMap
+subMap(K from, boolean, K to, boolean) NavigableMap
+headMap(K to, boolean) NavigableMap
+tailMap(K from, boolean) NavigableMap
}
class AbstractMap~K,V~ {
<<abstract>>
}
class TreeMap~K,V~ {
-Entry root
-int size
-Comparator comparator
+put(K, V) V
+get(Object) V
+remove(Object) V
+lowerKey(K) K
+floorKey(K) K
+ceilingKey(K) K
+higherKey(K) K
}
Map <|-- SortedMap
SortedMap <|-- NavigableMap
AbstractMap <|-- TreeMap
NavigableMap <|.. TreeMap
TreeMap ..|> Cloneable : implements
TreeMap ..|> Serializable : implements
图表说明:
- 第一层:Map 家族接口演化
Map 是所有映射的根接口;SortedMap 扩展了 Map,提供了firstKey、lastKey以及基于SortedMap返回类型的范围视图方法(subMap、headMap、tailMap)。NavigableMap 进一步扩展了 SortedMap,增加了导航方法(lowerKey、floorKey等)和能够控制边界包含性的范围视图(subMap(K, boolean, K, boolean)等)。 - 第二层:TreeMap 实现层级
TreeMap直接继承了AbstractMap,并实现了NavigableMap,从而获得了完整的排序、导航和范围视图能力。类图中展示了 TreeMap 的核心字段和方法,如root、size、comparator以及重要的导航方法。 - 关键结论强调
NavigableMap 是 TreeMap 功能的核心来源,它让 TreeMap 不再是简单的有序 Map,而是一个支持“搜索最近键”的有序集合视窗。这也是 TreeMap 区别于普通 SortedMap 实现的关键优势。
Part 2:存储与构造篇
模块 3:存储结构与核心字段(源码剖析)
TreeMap 的底层是一棵红黑树,每个节点由内部类 Entry<K,V> 表示。红黑树是一种自平衡的二叉查找树,通过以下五条性质保证最坏情况下的高效性:
- 每个节点非红即黑。
- 根节点始终为黑色。
- 每个叶子节点(NIL)为黑色。
- 红色节点的两个子节点必须都是黑色(路径上不能有连续红色)。
- 从任一节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。
Entry 节点结构(JDK 8 TreeMap.Entry):
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK; // 默认为黑
}
TreeMap 核心字段:
public class TreeMap<K,V> extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
private final Comparator<? super K> comparator; // 可为null(自然排序)
private transient Entry<K,V> root; // 根节点
private transient int size = 0; // 键值对数量
private transient int modCount = 0; // 结构修改次数
}
classDiagram
class TreeMap~K,V~ {
-Comparator comparator
-Entry root
-int size
-int modCount
}
class Entry~K,V~ {
-K key
-V value
-Entry left
-Entry right
-Entry parent
-boolean color
}
TreeMap "1" *-- "1..*" Entry : root
图表说明:
- 第一层:TreeMap 与 Entry 的组合关系
类图明确展示了TreeMap持有指向根节点的root引用,形成了一对多的聚合关系。每个Entry节点包含键、值、左子、右子、父节点的引用以及颜色标记。 - 第二层:红黑树性质的体现
通过parent、left、right构成双向链表结构,便于向上回溯和旋转。boolean color直接对应红/黑状态,是实现自平衡的核心。 - 源码方法对应
所有操作(put、remove、get)均通过root递归遍历Entry的树链完成。modCount用于 fail-fast 检查,任何结构变更都会使其自增。 - 关键结论强调
每个节点的左/右子树高度差通过颜色规则来弹性控制,从而保证 O(log n) 的查询效率。这种设计比严格的平衡二叉树(AVL)减少了旋转次数,适用于频繁插入删除的场景。
模块 4:构造方法(源码剖析)
TreeMap 提供了四个构造器,初始化时可选择自然排序或定制比较器,并可批量导入其他 Map。
- TreeMap():使用自然排序,要求 Key 必须实现
Comparable。comparator字段为null。 TreeMap(Comparator<? super K> comparator):传入自定义Comparator,由其定义排序规则,优先级最高。TreeMap(Map<? extends K, ? extends V> m):从普通 Map 构造,会调用putAll内部遍历插入。由于comparator为 null,Key 必须实现 Comparable。- TreeMap(SortedMap<K, ? extends V> m):从有序 Map 构造,使用
buildFromSorted方法线性时间建树,并保留其比较器,效率高于逐个插入。
注意:构造函数本身不构建树结构,初始化时 root 为 null,真正的红黑树构建发生在插入数据时。
Demo 代码:不同构造方式演示
import java.util.*;
public class TreeMapConstructionDemo {
public static void main(String[] args) {
// 1. 自然排序构造
TreeMap<String, Integer> naturalMap = new TreeMap<>();
naturalMap.put("apple", 5);
naturalMap.put("banana", 3);
System.out.println("自然排序:" + naturalMap); // {apple=5, banana=3}
// 2. 自定义比较器(按字符串长度排序)
TreeMap<String, Integer> customMap = new TreeMap<>(
Comparator.comparingInt(String::length)
);
customMap.put("pear", 4);
customMap.put("kiwi", 2);
// 注意:长度相同会覆盖旧值
customMap.put("date", 7); // 会覆盖 "kiwi" 因为长度都是4,比较结果为0
System.out.println("长度排序:" + customMap); // {pear=4, date=7}
// 3. 从普通HashMap构造(Key必须实现Comparable)
Map<String, Double> hashMap = new HashMap<>();
hashMap.put("zoo", 9.0);
hashMap.put("ant", 1.0);
TreeMap<String, Double> fromMap = new TreeMap<>(hashMap);
System.out.println("从HashMap构造(自动排序):" + fromMap); // {ant=1.0, zoo=9.0}
// 4. 从SortedMap构造
TreeMap<String, Double> existSorted = new TreeMap<>(naturalMap);
TreeMap<String, Double> fromSorted = new TreeMap<>(existSorted);
System.out.println("从SortedMap构造(保留顺序):" + fromSorted);
}
}
Part 3:核心原理篇
模块 5:put 操作——二叉查找与红黑树修复(源码剖析)
put(K key, V value) 实际委托给内部方法 put(K key, V value, boolean replaceOld),该方法执行标准的 BST 插入,并在必要时调用 fixAfterInsertion 恢复红黑树性质。
流程分阶段:
- 空树处理:若
root为 null,直接创建根节点,染黑(满足性质2),返回。 - 递归比较:定义比较器
cpr = comparator,若cpr != null则使用cpr.compare(key, t.key),否则强制转换为Comparable调用compareTo。沿着根节点向下,若比较结果小于0走左子树,大于0走右子树。 - 更新或插入:若遍历中遇到
compare结果为0的节点,说明键“相等”,替换旧值(若replaceOld为 true)。否则,最终到达叶子位置,创建新Entry并作为左或右子节点挂到父节点上。 - 自平衡修复:插入新节点为红色,调用
fixAfterInsertion(e)。进入循环,当父节点为红色时违反性质4,需要根据叔叔节点的颜色分情况处理:- Case 1:叔叔节点是红色 → 父、叔染黑,祖父染红,当前节点上移至祖父。
- Case 2:叔叔黑色,且当前节点是右孩子 → 左旋父节点,转换为 Case 3。
- Case 3:叔叔黑色,且当前节点是左孩子 → 父染黑,祖父染红,右旋祖父。 循环结束后,确保根节点为黑色。
简化版源码对应(JDK 8 fixAfterInsertion 逻辑):
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x))); // 叔叔
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else { // 对称情况
// ...
}
}
root.color = BLACK;
}
graph TD
Start(["put key value"]) --> RootNull{"根节点为空吗"}
RootNull -->|"是"| CreateRoot["创建新条目作为根节点并染黑"]
CreateRoot --> ReturnOldNull["返回空"]
RootNull -->|"否"| Loop["从根节点开始比较"]
Loop --> CmpResult{"比较结果"}
CmpResult -->|"小于零"| GoLeft["沿左子树继续"]
CmpResult -->|"大于零"| GoRight["沿右子树继续"]
CmpResult -->|"等于零"| UpdateValue["替换值并返回旧值"]
GoLeft --> HasLeft{"左子存在吗"}
HasLeft -->|"是"| Loop
HasLeft -->|"否"| InsertLeft["创建新条目为左子"]
GoRight --> HasRight{"右子存在吗"}
HasRight -->|"是"| Loop
HasRight -->|"否"| InsertRight["创建新条目为右子"]
InsertLeft --> FixInsert["调用修复插入"]
InsertRight --> FixInsert
FixInsert --> CheckParent{"父节点是红色吗"}
CheckParent -->|"否"| Done["插入完成 返回空"]
CheckParent -->|"是"| UncleCase{"叔叔节点颜色"}
UncleCase -->|"红色"| Recolor["父叔变黑 祖父变红 上移到祖父"]
Recolor --> CheckParent
UncleCase -->|"黑色"| RotateCase{"当前节点是右子吗"}
RotateCase -->|"是"| LeftRot["左旋父节点"]
LeftRot --> RecolorParent["父变黑 祖父变红"]
RecolorParent --> RightRot["右旋祖父"]
RotateCase -->|"否"| RecolorParent
RightRot --> Done
图表说明:
- 第一层:分阶段描述 put 全流程
流程图清晰分为 根节点空查、递归比较并插入、红黑树修复 三个阶段,体现put方法内部的for循环查找和尾部插入逻辑。 - 第二层:修复阶段的三种情况
当新节点插入后触发修复,依据叔叔节点颜色划分:叔叔红 仅需变色并上移;叔叔黑 且当前节点为内侧孩子时,先局部旋转转变为外侧情况,再统一执行父黑、祖父红、旋转祖父的操作。 - 源码方法对应
流程与put(K, V, true)和fixAfterInsertion(Entry)完全对应。Case 1 对应colorOf(y)==RED分支,Case 2/3 对应else中的if (x == rightOf(...))及其后的旋转。 - 关键结论强调
所有插入修复的旋转次数最多两次,变色则可能沿路径向上传播至根,但总的复杂度仍为 O(log n)。红黑树的优势在于插入旋转次数相比 AVL 树更少。
模块 6:remove 操作——节点删除与平衡修复(源码剖析)
remove(Object key) 通过 getEntry(key) 定位节点,若存在则调用 deleteEntry(Entry<K,V> p)。删除操作在二叉查找树基础上需处理节点孩子数量,并调用 fixAfterDeletion 恢复黑平衡。
删除的三种情况:
- 无子节点:直接删除,若节点为黑则调用
fixAfterDeletion的位置为其本身(已脱离树,但仍传参与修复)。 - 有一个子节点:用子节点替换删除节点,若删除节点为黑则修复这个子节点。
- 有两个子节点:找到后继节点(右子树最小值),拷贝后继的键值到删除节点,然后将问题转化为删除只有一个右子节点的后继节点(因为后继肯定没有左子),此时退化到情况1或2。
fixAfterDeletion 在跳出循环前不断调整,以确保被删除黑节点所在路径的黑高平衡。主要逻辑基于兄弟节点颜色及兄弟子节点颜色进行兄弟变红、旋转等操作。
简化的 deleteEntry 结构:
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// 有两个孩子,转换为删除后继
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s; // 重定向到后继
}
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) { // 单子情况
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
p.left = p.right = p.parent = null;
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // 无子且为根
root = null;
} else { // 无子非根
if (p.color == BLACK)
fixAfterDeletion(p);
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else
p.parent.right = null;
p.parent = null;
}
}
}
graph TD
Start(["remove key"]) --> FindNode["getEntry 找到节点 p"]
FindNode --> TwoChild{"p 有两个孩子吗"}
TwoChild -->|"是"| Successor["获取后继 s"]
Successor --> CopyData["将 s 的键和值复制到 p"]
CopyData --> Redirect["令 p = s"]
Redirect --> DelSingle
TwoChild -->|"否"| DelSingle["确定替换节点 replacement"]
DelSingle --> HasReplacement{"替换节点不为空吗"}
HasReplacement -->|"是"| ReplaceParent["替换父指针"]
ReplaceParent --> CheckBlackDel{"被删节点 p 是黑色吗"}
CheckBlackDel -->|"是"| FixDel["fixAfterDeletion replacement"]
CheckBlackDel -->|"否"| Finish
HasReplacement -->|"否"| IsRoot{"p 是根节点吗"}
IsRoot -->|"是"| ClearRoot["root = null"]
IsRoot -->|"否"| CheckBlackNoChild{"被删节点 p 是黑色吗"}
CheckBlackNoChild -->|"是"| FixDelSelf["fixAfterDeletion p"]
CheckBlackNoChild -->|"否"| Unlink["解除父节点引用"]
FixDelSelf --> Unlink
Unlink --> Finish
FixDel --> Finish(["删除完成"])
ClearRoot --> Finish
图表说明:
- 第一层:删除情况收敛
流程图展示了deleteEntry的分支逻辑:双子树 通过后继替换转换为单子树或无子树情况,统一后续处理。 - 第二层:修复触发的条件
只有当被删除节点(或替代节点)为黑色时,才会调用fixAfterDeletion。因为删除黑色节点会破坏黑高平衡,需要修复。修复后的解除链接操作保证树结构完整。 - 源码方法对应
与deleteEntry(Entry)及fixAfterDeletion(Entry)对应。successor方法找到中序后继。修复代码中根据兄弟节点颜色等分情况处理,可能涉及左旋、右旋和变色,与插入修复对称但更复杂。 - 关键结论强调
删除修复可能需要多次旋转和变色,最坏情况沿树向上传播,复杂度仍为 O(log n)。后继替代策略使删除只涉及至多一个子树节点的实际操作,简化了逻辑。
模块 7:get 与导航方法(源码剖析)
get 操作 由 get(Object key) 入口,内部调用 getEntry(key) 方法。该方法利用红黑树的二叉查找特性:从根节点开始,使用比较器或自然比较法比较 key,小于走左子,大于走右子,等于则返回该节点。若到达叶子仍找不到,返回 null。时间复杂度 O(log n)。
导航方法(如 lowerKey、floorKey)的实现类似,但会在搜索中记录最近候选节点。以 getLowerEntry(对应 lowerKey)为例:
- 从根向下搜索,若当前节点 key 小于指定 key,则当前节点成为候选,并转向右子树尝试找更大但仍小于 key 的值。
- 若当前节点 key 大于等于指定 key,则转向左子树。
- 最终返回候选节点。
所有导航方法(lowerKey、floorKey、ceilingKey、higherKey)都基于此“带偏好的二分查找”策略,均能保证 O(log n)。
graph TD
Start(["getEntry key"]) --> RootCheck{"根节点为空吗"}
RootCheck -->|"是"| RetNull["返回空"]
RootCheck -->|"否"| Curr["当前节点为根节点"]
Curr --> Cmp{"比较键值与当前节点的键"}
Cmp -->|"小于零"| GoLeft["当前节点指向左子"]
Cmp -->|"大于零"| GoRight["当前节点指向右子"]
Cmp -->|"等于零"| RetCurr["返回当前节点"]
GoLeft --> NullLeft{"当前节点为空吗"}
NullLeft -->|"是"| RetNull
NullLeft -->|"否"| Cmp
GoRight --> NullRight{"当前节点为空吗"}
NullRight -->|"是"| RetNull
NullRight -->|"否"| Cmp
图表说明:
- 第一层:标准二叉搜索树查找
get查找流程图中,从根节点开始,根据比较结果决定走向,直到找到匹配或 null。这正是有序二叉树的特性,使得 TreeMap 的查询复杂度与树高成正比。 - 第二层:导航方法的候选更新
导航方法如getLowerEntry在此基础上增加了一个**“潜在结果”变量**。当走向右子树时更新潜在结果,因为此时当前节点是所见最小的“小于 key”的节点;而走向左子树时不更新。最终返回潜在结果。 - 源码方法对应
getEntry使用循环而非递归。getLowerEntry等方法在TreeMap中均有 private 实现,逻辑同在。特殊处理了 comparator 为 null 的情况,使用Comparable的compareTo。 - 关键结论强调
所有查询和导航方法都不修改树结构,因此无需加锁就能实现 fail-fast 检测,且性能稳定为 O(log n)。导航方法使得 TreeMap 能快速回答“比X大的最小键”此类问题。
Part 4:导航与子视图篇
模块 8:NavigableMap 导航方法全景
NavigableMap 为 TreeMap 赋予了丰富的有序查询能力,使其不仅是一个 Map,更是一个有序数据的搜索工具。
常用导航方法列表:
- lowerKey(K key):返回严格小于给定键的最大键,若无则 null。
- floorKey(K key):返回小于等于给定键的最大键。
- ceilingKey(K key):返回大于等于给定键的最小键。
- higherKey(K key):返回严格大于给定键的最小键。
- firstKey() / lastKey():返回最小/最大键。
- pollFirstEntry() / pollLastEntry():获取并移除最小/最大键值对。
- descendingKeySet() / descendingMap():返回逆序视图。
Demo 代码:导航方法深度使用
TreeMap<Integer, String> map = new TreeMap<>();
map.put(10, "A");
map.put(20, "B");
map.put(30, "C");
map.put(40, "D");
System.out.println("lowerKey(25): " + map.lowerKey(25)); // 20
System.out.println("floorKey(20): " + map.floorKey(20)); // 20
System.out.println("ceilingKey(25): " + map.ceilingKey(25)); // 30
System.out.println("higherKey(30): " + map.higherKey(30)); // 40
System.out.println("firstKey: " + map.firstKey()); // 10
System.out.println("lastKey: " + map.lastKey()); // 40
// pollFirstEntry 移除最小元素
Map.Entry<Integer, String> firstEntry = map.pollFirstEntry();
System.out.println("移除 first: " + firstEntry.getValue()); // A
System.out.println("新 firstKey: " + map.firstKey()); // 20
// 逆序导航
NavigableMap<Integer, String> descMap = map.descendingMap();
System.out.println("逆序首个键: " + descMap.firstKey()); // 40
模块 9:子映射视图——subMap/headMap/tailMap
TreeMap 支持返回与主映射共享底层树的子映射视图,视图的修改会直接影响原 TreeMap,反之亦然。这些视图由内部类 AscendingSubMap 和 DescendingSubMap 实现。
- subMap(K from, boolean fromInclusive, K to, boolean toInclusive):返回从
from到to的部分映射。 - headMap(K to, boolean inclusive):键小于(或等于)
to的部分。 - tailMap(K from, boolean inclusive):键大于(或等于)
from的部分。
这些方法返回的视图实现了 NavigableMap,支持在子范围内进行进一步的导航和操作。插入时若超出范围会抛出 IllegalArgumentException。
flowchart LR
Main[TreeMap实例 root] --> Sub[AscendingSubMap 视图]
Sub --> MainTree[委托操作到原TreeMap方法]
MainTree --> Root[共享同一个 root Entry]
Sub --> BoundCheck[进行边界检查]
BoundCheck -->|超出范围| Throw[IllegalArgumentException]
图表说明:
- 第一层:视图与主映射的共享关系
AscendingSubMap内部持有对原TreeMap的引用和边界字段(fromStart、toEnd等)。所有操作(put、get、remove)最终调用原 TreeMap 的相应方法,并在入口和出口进行边界范围检查。 - 第二层:边界检查机制
视图中的put方法会检查插入的 key 是否在[from, to)或(from, to]范围内,不满足则抛出IllegalArgumentException。读取操作如果 key 超出范围,返回 null 或抛出NoSuchElementException(视方法而定)。 - 关键结论强调
子映射视图并非数据副本,而是过滤窗口。修改视图会直接修改底层树,同样,外部对原 Map 的修改也会影响视图。这种设计节省内存,但需注意并发修改和范围边界问题。
Part 5:对比与陷阱篇
模块 10:TreeMap vs HashMap vs LinkedHashMap——无序与有序的博弈
graph TD
Start{"需要键有序吗"}
Start -->|"是"| Tree["TreeMap 红黑树 Olog n 有序"]
Start -->|"否"| InsertOrder{"需要维护插入顺序吗"}
InsertOrder -->|"是"| Linked["LinkedHashMap 哈希表加链表 O1 插入顺序"]
InsertOrder -->|"否"| HashMap["HashMap 哈希表 O1 无序"]
图表说明:
- 第一层:功能差异导致选型
TreeMap 的独特卖点是有序排列和导航,这是 HashMap 和 LinkedHashMap 不具备的。HashMap 提供最快存取,LinkedHashMap 在快速存取的同时保留了插入顺序或访问顺序。 - 第二层:底层结构与性能对比
- TreeMap:红黑树,每个操作 O(log n),节点内部字段多(left/right/parent/color),内存开销较大但无预留空间。
- HashMap:数组+链表/红黑树(JDK8+),理想情况下 O(1),扩容时有额外开销,内存利用效率取决于负载因子。
- LinkedHashMap:继承 HashMap,用链表维护顺序,轻微增加内存和性能开销(O(1) 仍然恒定)。
- 关键结论强调
若无排序需求,切忌使用 TreeMap。对仅需快速查找的 Key,HashMap 是绝对首选。当需要保持插入顺序时,LinkedHashMap 提供比 TreeMap 更高效的 O(1) 操作,因为它不需要按比较权重排序,仅维护链表。
模块 11:TreeMap vs ConcurrentSkipListMap——非并发与并发有序
ConcurrentSkipListMap 是 Java 并发包中提供的基于跳表的有序映射,它同样是 NavigableMap 的实现,但线程安全且读操作无锁(基于 volatile 和 CAS),写操作使用 CAS 和细粒度锁。
| 特性 | TreeMap | ConcurrentSkipListMap |
|---|---|---|
| 线程安全 | 否(需外部同步) | 是(无锁读,CAS写) |
| 底层结构 | 红黑树 | 跳表 |
| 有序性 | 是 | 是 |
| 导航方法 | 支持 | 支持 |
| 空键支持 | 取决于比较器 | 不支持 null 键 |
| 迭代器 | fail-fast | weakly consistent(不抛异常) |
| 并发性能 | 差,全表同步 | 优秀,高并发 |
关键结论:在单线程环境下 TreeMap 性能略优(树结构更紧凑),但在多线程或需要迭代过程中被修改的场景,ConcurrentSkipListMap 是唯一正确的选择。切勿企图用 Collections.synchronizedSortedMap 包装 TreeMap 获得高并发性能,因为全表锁会导致严重瓶颈。
模块 12:常见陷阱与最佳实践
陷阱 1:Key 未实现 Comparable 且未传 Comparator
- 错误代码:
TreeMap<Object, String> map = new TreeMap<>();
map.put(new Object(), "value"); // 抛出 ClassCastException
- 解决方案:确保 Key 实现
Comparable,或在构造时提供Comparator。
陷阱 2:比较器与 equals 不一致
TreeMap 使用 compare 判等,Map 契约中的 containsKey 官方文档建议“一致性”。若 compare(e1, e2) == 0 但 !e1.equals(e2),则 containsKey 判断存在而 equals 判断不等会造成混乱。例如使用忽略大小写的比较器时,“ABC”和“abc”被视为相同键。
陷阱 3:可变对象作 Key 并修改比较字段 示例:
TreeMap<User, String> map = new TreeMap<>(Comparator.comparing(User::getAge));
User u = new User("Alice", 25);
map.put(u, "A");
u.setAge(30); // 修改比较字段
map.get(u); // 返回 null,无法按新值找到
破坏树结构,导致不可预知行为。
陷阱 4:多线程并发修改
多个线程同时 put/remove,可能破坏红黑树结构,或导致 ConcurrentModificationException。必须用 ConcurrentSkipListMap 或外部同步。
陷阱 5:null Key 问题
若未提供处理 null 的比较器,put(null, value) 在比较时 NPE。可自定义容许 null 的比较器,但 null 键将失去比较意义,一般不推荐。
Part 6:总结与面试篇
模块 13:注意事项与性能总结
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| put | O(log n) | 插入加平衡修复 |
| get | O(log n) | 二叉查找 |
| remove | O(log n) | 删除加平衡修复 |
| lowerKey/floorKey 等 | O(log n) | 搜索+候选记录 |
| subMap 视图操作 | O(log n) | 共享树,带边界检查 |
| 迭代 (keySet.iterator) | O(n) | 中序遍历,每个节点 O(1) 但整体 O(n) |
内存占用:每个 Entry 除键值外额外存储 4 个引用(left, right, parent)和一个 boolean(实际可能以 int 存储),相比 HashMap.Node 开销大得多。但在数据量不大且需要排序时,这点代价可接受。
最佳实践:
- 始终在构造时明确提供
Comparator,避免对自在 Key 的Comparable依赖,使逻辑更清晰。 - 不要修改已放入 TreeMap 的 Key 的关键比较字段。
- 需要范围查询、排行榜时首选 TreeMap。
- 考虑并发时直接切换到
ConcurrentSkipListMap。
好的,这是为您详细扩展后的“模块 14:面试高频专题”内容,已保持与前文一致的格式和深度,同时大幅增加了每个问题的详尽程度。
模块 14:面试高频专题
以下内容集中了关于 TreeMap 的十大高频面试考点,每个问题都包含标准回答、追问模拟、加分回答,并从源码、原理、实战等维度进行深度剖析。
1. TreeMap 的底层数据结构是什么?红黑树有哪些性质?
标准回答: TreeMap 的底层数据结构是红黑树,它是一种自平衡的二叉查找树,能够保证在最坏情况下基本动态操作的时间复杂度为 O(log n)。红黑树满足以下五条性质:
- 每个节点要么是红色,要么是黑色。
- 根节点始终为黑色。
- 每个叶子节点(NIL)为黑色。
- 如果一个节点是红色的,那么它的两个子节点必须都是黑色(即路径上不能出现连续的红色)。
- 从任一节点到其每个叶子节点的所有简单路径都包含相同数目的黑色节点。
这些性质保证了树的大致平衡:最长路径(红黑交替)不会超过最短路径(全黑)的两倍,因此树高被控制在 2log(n+1) 以内。
追问模拟:
-
追问:为什么 TreeMap 选择红黑树而不是 AVL 树?
回答:AVL 树追求更严格的平衡(左右子树高度差不超过 1),在频繁插入删除的场景下,AVL 需要的旋转次数往往比红黑树多,开销更大。红黑树通过牺牲一部分严格平衡性,换取了写入效率,更适合大多数通用场景。Java 的HashMap在链表转树时同样使用红黑树,也是出于性能考量。 -
追问:红黑树的“黑高”是什么概念?有什么作用?
回答:黑高指从某个节点出发(不含该节点)到叶子节点的任意路径上黑色节点的数量。性质 5 保证所有路径黑高相等,这就是“黑平衡”。黑高是衡量红黑树平衡性的关键指标,自平衡修复的核心就是维护局部黑高不变。
加分回答:
- 源码映射:在 JDK 8 的
TreeMap中,节点通过内部类Entry<K,V>表示,颜色通过boolean color字段定义(BLACK = true)。fixAfterInsertion和fixAfterDeletion方法中的自平衡逻辑,就是经典的“变色+旋转”,严格按 CLRS 算法实现。 - 旋转的本质:源码中
rotateLeft和rotateRight两个方法通过改变left、right、parent指针实现局部树形结构的调整,不改变键的二叉查找性质,只改变高度和颜色分布。
2. TreeMap 如何实现键的有序性?自然排序和比较器排序的区别?
标准回答: TreeMap 通过两种方式确定键的顺序:
- 自然排序:要求键的类实现
Comparable接口,TreeMap 调用键的compareTo方法来比较。 - 比较器排序:在构造 TreeMap 时传入自定义
Comparator,所有比较使用comparator.compare(key1, key2)。
区别:
- 优先级上,
Comparator的优先级高于Comparable。若构造时传入了 Comparator,即使 Key 实现了 Comparable,也会优先使用 Comparator。 - 灵活性上,Comparator 允许对未实现 Comparable 的类定义顺序,或定义与自然顺序不同的排序规则(如降序、按字符串长度等)。
追问模拟:
-
追问:如果 Key 既没实现 Comparable,构造时又没传 Comparator,会怎样?
回答:在put等操作中,TreeMap 会尝试将 Key 强制转换为Comparable并调用compareTo,此时会抛出ClassCastException。源码中可见:当comparator为 null 时,会执行((Comparable<? super K>)k1).compareTo((K)k2)。 -
追问:Comparator 的 compare 方法和 Key 的 equals 方法有什么关系?
回答:TreeMap 内部完全依赖compare或compareTo返回 0 来判断“相等”,当三者不一致时会导致困惑。Java 官方建议比较器与 equals 保持一致,但 TreeMap 并不强制。
加分回答:
- 定制比较器的妙用:可以传入
Comparator.comparingInt(String::length)实现按长度排序,或使用Collections.reverseOrder()快速得到反向映射。也可以使用Comparator.nullsFirst()来安全处理 null 键(如果允许)。 - 源码快照:TreeMap 的
getEntry方法第一行就是获取Comparator cpr = comparator;,之后的循环中根据 cpr 是否为 null 走不同比较分支,决定向左或向右移动。
3. TreeMap 的 put 方法执行流程?插入后如何保持平衡?
标准回答:
put(K key, V value) 的执行分为三个阶段:
- 空树检查:若根节点为空,直接创建新节点并将颜色置黑作为根,插入完成。
- 二叉查找插入点:从 root 开始,使用 Comparator 或 Comparable 比较键的值。若 key 小于当前节点,遍历左子树;大于则遍历右子树;若相等则更新 value 并返回旧值。一直找到叶子节点,将新节点作为左孩子或右孩子挂载,颜色初始为红色。
- 自平衡修复:调用
fixAfterInsertion(e)。进入循环,当新节点的父节点为红色(违反性质 4)时,根据叔叔节点的颜色分情况处理:- Case 1:叔叔为红 → 将父、叔染黑,祖父染红,并将当前节点上移到祖父。
- Case 2:叔叔为黑,且当前节点是父的内侧孩子(如父是左子,当前是右子)→ 对父做左旋,变成 Case 3。
- Case 3:叔叔为黑,且当前节点是父的外侧孩子(如父左子,当前也是左子)→ 父染黑,祖父染红,右旋祖父。 跳出循环后,将根节点强制染黑以确保性质 2。
追问模拟:
-
追问:为什么新插入的节点默认是红色的?
回答:如果插入黑色节点,必然会破环性质 5(增加路径黑点数),修复代价涉及整个子树,非常困难。而插入红色节点,可能只破坏性质 4(连续红),其修复相对局部,代价更小。 -
追问:插入修复最多需要几次旋转?
回答:最多两次旋转。Case 1 只变色不上移,可能循环多次;一旦进入 Case 2/Case 3 的旋转分支,旋转完成后树立即恢复平衡,循环中止。
加分回答:
- 源码对应:
put方法最终委派给private V put(K key, V value, boolean replaceOld)。修复方法fixAfterInsertion(Entry<K,V> x)中的 while 条件为x != null && x != root && x.parent.color == RED。 - 旋转源码追踪:
rotateLeft(Entry<K,V> p)先记录右孩子r,将r.left作为p的新右子树,然后p.parent指向r,最后r.left = p。所有指针更新均 O(1)。
4. TreeMap 如何处理键重复?依赖什么判定相等?
标准回答:
TreeMap 不使用 hashCode() 和 equals() 来判断键是否重复,而是依赖 compare 或 compareTo 方法返回 0 来判定键相等。当 put 操作遇到 compare 结果为 0 的已有节点时,会用新 value 替换旧 value,并返回旧值。因此,即使两个对象的 equals 返回 false,只要比较器认为它们相等,TreeMap 就会视为同一个键。
追问模拟:
-
追问:这会导致什么问题?
回答:最典型的是Map接口规定的containsKey行为与用户预期不一致。例如:如果定义了“忽略大小写”的比较器,那么map.put("ABC",1); map.containsKey("abc");返回 true,但"ABC".equals("abc")是 false。这可能造成歧义。 -
追问:那如果我想用
equals判断重复,应该怎么做?
回答:如果确实希望用equals约束,那么自定义比较器时必须保证compare(e1, e2)==0当且仅当e1.equals(e2)。这通常需要比较器比较所有用于 equals 的字段,并且对 null 做处理。否则,不建议混用 TreeMap 和依赖 equals 的逻辑。
加分回答:
- 源码证据:
put方法中查找过程:do { cmp = cpr.compare(k, p.key); ... } while (p != null);。若cmp == 0,则执行p.value = value; return oldValue;,完全没有调用equals。 - 最佳实践:保持
compareTo和equals一致的类(如String、Integer)才是 TreeMap 键的最佳候选人。避免使用字段可变的类作为键。
5. TreeMap 和 HashMap 的区别?如何选择?
标准回答: 核心区别体现在底层结构、时间复杂度、有序性、内存开销等维度:
- 数据结构:HashMap 基于哈希表(数组+链表/红黑树,JDK8+),TreeMap 基于红黑树。
- 时间复杂度:HashMap 理论上提供 O(1) 的 get/put,但在哈希冲突严重时退化为 O(log n) 或 O(n)(取决于树化);TreeMap 稳定提供 O(log n)。
- 有序性:HashMap 无序,TreeMap 按键排序。
- Key 要求:HashMap 的 Key 必须正确实现
hashCode和equals;TreeMap 要求 Key 可比较(自然或比较器)。 - null 键:HashMap 允许一个 null 键;TreeMap 默认不允许,但可通过能处理 null 的比较器支持(不推荐)。
选择指南:当需要快速存取且不关心顺序时,选 HashMap;当需要按键排序、范围查询、导航操作时,必须选 TreeMap。
追问模拟:
-
追问:内存占用上哪个更大?
回答:TreeMap 单个节点存储了 left、right、parent、color 四个额外字段,开销明显大于 HashMap 的 Node(hash、key、value、next)。但 HashMap 会预留一部分数组空间(负载因子 0.75),而 TreeMap 按需分配。总体来看,大量数据下 HashMap 内存利用更紧凑。 -
追问:TreeMap 能用于缓存场景吗?
回答:可以,但通常不用。O(log n) 比 O(1) 慢,且缓存常需淘汰策略,LinkedHashMap 提供访问顺序及 LRU 实现,比 TreeMap 更合适。
加分回答:
- 退化情况对比:HashMap 在实现不佳的
hashCode下可能退化;TreeMap 永远不退化,但操作始终带上 log 因子。 - 结合使用:有时可以先建立 HashMap 快速构建,然后通过
new TreeMap(map)获得排序视图用于输出。
6. TreeMap 是否允许 null 键?为什么?
标准回答:
TreeMap 本身没有强制禁止 null 键,但其自然排序机制通常不允许。因为当 comparator 为 null 时,put 操作会调用 Comparable.compareTo,如果键为 null 则抛出 NullPointerException。如果传入了一个能处理 null 值的自定义 Comparator(例如 Comparator.nullsFirst(Comparator.naturalOrder())),则 null 键可以被添加到 TreeMap 中。但即便如此,null 键在一个排序映射中几乎没有实际意义,且容易引发许多边界问题,因此强烈不推荐。
追问模拟:
-
追问:ConcurrentSkipListMap 允许 null 键吗?
回答:不允许。ConcurrentSkipListMap 全线禁止 null 键(和 null 值),因为在并发环境下,null 作为返回值有歧义(到底是映射中没有该键,还是值就是 null?)。 -
追问:那 TreeMap 的 null 值呢?
回答:TreeMap 允许 null 值,但需要注意,当使用get获取到 null 时,可能是键不存在,也可能是值就是 null。这方面与 HashMap 行为一致。
加分回答:
- 源码视角:TreeMap 的
getEntry方法中,若 comparator 为 null,直接调用((Comparable<? super K>) k).compareTo(p.key),k 为 null 必然 NPE。 - 防御性编程:如果必须允许 null 键(如适配遗留接口),应该显式提供一个 null-safe 的比较器,并在团队文档中写清边界语义。否则,避免使用。
7. TreeMap 的导航方法(lower/floor/ceiling/higher)如何实现?
标准回答:
所有导航方法均基于二叉查找树的有序遍历特性。以 lowerKey(严格小于给定键的最大键)为例,调用的是内部方法 getLowerEntry。具体流程:
- 从根节点开始循环。
- 比较给定 key 和当前节点 key:
- 如果当前键 < 给定键,则当前节点成为一个候选节点,并向右子树移动(试图找一个更接近但仍小于 key 的键)。
- 如果当前键 >= 给定键,则向左子树移动(当前节点不满足条件,需要更小的键)。
- 当循环结束时(节点为 null),返回最后记录的候选节点。
类似地,floorKey(小于等于)的区别在于当比较结果为 0 时直接返回当前节点;ceilingKey 与 higherKey 逻辑对称。
由于红黑树高度平衡,这些操作均能在 O(log n) 时间内完成。
追问模拟:
-
追问:如果 TreeMap 为空,导航方法返回什么?
回答:绝大部分返回 null(如 lowerKey、floorKey 等),但firstKey和lastKey会抛出NoSuchElementException。 -
追问:
pollFirstEntry和firstKey有什么区别?
回答:firstKey只是获取最小的键,不改变映射。pollFirstEntry不仅返回最小键的键值对,还会将该条目从树中删除,类似于Queue的poll操作,复杂度也是 O(log n)。
加分回答:
- 内部调用链:
lowerKey→getLowerEntry→ 私有方法,直接操作Entry。getCeilingEntry等逻辑见 JDK 源码。 - 性能稳定:与
get方法类似,导航方法不存在结构性修改,不需要平衡修复,性能很稳定且不涉及任何锁。
8. 比较器和 equals 不一致会导致什么问题?
标准回答:
当比较器 compare(e1, e2) == 0 但 !e1.equals(e2) 时,TreeMap 的行为会偏离 Map 接口的“理想”语义。
- 首先,TreeMap 会将两个不相等的对象视为同一个键,put 时发生覆盖,导致“丢失”一个键值对。
- 其次,如果先 put(e1) 再 get(e2),会返回 e1 对应的值,因为查找只依赖比较器。但若外层逻辑使用
equals判断,例如调用containsKey(e2)TreeMap 返回 true,但调用者认为 e2 不应存在,逻辑产生矛盾。 - 更严重的是,如果随后用
e1的 equals 逻辑修改映射,可能找不到 e2,导致孤立数据。
追问模拟:
-
追问:能举个具体例子吗?
回答:假设有大小写不敏感比较器,map.put("Java", 1); map.put("java", 2);,则 TreeMap 的大小为 1,键 "Java" 对应的值为 2。此时map.containsKey("JAVA")为 true。但如果你用一个equals严格区分大小写的集合并期望包含两个不同条目,就会失败。 -
追问:HashMap 会有类似问题吗?
回答:不会,HashMap 使用hashCode和equals,对 Key 对象的相等性判断完全遵循Object.equals语义,不会出现这种矛盾。
加分回答:
- JDK 官方警告:在
SortedMap的文档中明确提到,为了正确实现SortedMap接口,比较器应与 equals 保持一致。这并非强制,仅仅因为“不这样做不会造成映射错误,只是性能偏差”,但对 TreeMap 而言,不一致会让containsKey产生假阳性。 - 规避策略:设计比较器时,比较所有参与 equals 判断的字段,并保证当字段完全一致时 compare 返回 0。这也是
Comparator接口文档中的强烈建议。
9. TreeMap 线程安全吗?并发中有什么替代?
标准回答:
TreeMap 不是线程安全的。如果多个线程同时读写,且至少一个线程在结构上进行修改(put/remove),必须外部同步。通常可以包装为 Collections.synchronizedSortedMap(new TreeMap(...)),但这会对所有方法加锁,并发性能极低,适用于低并发场景。
在高并发环境下的有序映射,正确的替代是 ConcurrentSkipListMap。它基于跳表数据结构,实现了 ConcurrentNavigableMap 接口,支持无锁读(通过 volatile 变量)和 CAS 写,能提供比同步 TreeMap 高得多的吞吐量,且迭代器是弱一致性的,不会抛出 ConcurrentModificationException。
追问模拟:
-
追问:为什么不用同步包装的 TreeMap 做并发?
回答:同步包装的 TreeMap 每个方法都争用同一个内置锁,读读互斥,会造成严重的锁竞争和线程阻塞。而 ConcurrentSkipListMap 读操作无锁,写使用细粒度的 CAS,极大地提升并发度。 -
追问:跳表和红黑树在并发上有什么优劣?
回答:跳表通过多层链表结构分散了修改热点,允许对局部节点进行 CAS 操作而不影响其他部分,天然适合无锁并发设计。红黑树修改需要旋转,影响范围较大,很难高效地实现细粒度锁或无锁。
加分回答:
- 源码对比:
ConcurrentSkipListMap的doPut方法使用循环 CAS 插入新节点,并可能通过提升节点层级建立索引;而 TreeMap 的fixAfterInsertion全程依赖指针修改,无并发控制。 - 弱一致性迭代器:ConcurrentSkipListMap 的迭代器不会抛出 CME,可能遍历到部分更新,但能安全遍历,这是并发容器的重要特征。
10. 为什么修改 TreeMap 中键的字段会导致严重问题?如何避免?
标准回答: TreeMap 的底层红黑树依赖键的比较结果来决定节点在树中的位置。如果键是一个可变对象,并且在放入 TreeMap 后,其参与比较的关键字段发生了变化,那么它在树中的自然位置就可能改变,但树结构本身不会自动调整。这将导致:
- 查找失败:后续用修改过的键或某个值去
get,根据比较器会走到另一条路径,找不到该节点,返回 null。 - 树结构破坏:如果新值导致节点违反排序性质,后续的插入、删除可能基于错误的前提,造成无法预期的结果甚至无限循环。
避免方法:
- 使用不可变对象作为键(如
String、Integer、LocalDate),它们的状态在创建后无法修改,最安全。 - 如果必须使用可变对象,在修改其比较字段前,先从 TreeMap 中
remove掉该条目,修改后再put回去。 - 创建副本:put 时存放对象的防御性副本,这样外部对原对象的修改不影响映射内部。
追问模拟:
-
追问:如果我只修改了不参与比较的字段,会有问题吗?
回答:不会直接影响查找和树结构,因为 TreeMap 只依赖比较字段定位节点。但如果这个字段影响equals且你的代码依赖 equals,可能产生二义性,但不会破坏树。 -
追问:HashMap 也会遇到类似问题吗?
回答:会的!HashMap 依赖hashCode和equals。如果修改了影响hashCode的字段,该键的存储桶位置会改变,导致 get 时找不到,同样会造成内存泄漏式的孤立节点。因此,不可变键对所有 Map 都是最佳实践。
加分回答:
- 示例代码:
TreeMap<User, String> map = new TreeMap<>(Comparator.comparingInt(User::getAge));
User u = new User("Alice", 25);
map.put(u, "A");
u.setAge(30); // 危险!
System.out.println(map.get(u)); // 很可能 null
- 根本原因:红黑树的查找基于比较结果的符号走向左或右。一旦 Key 字段修改,比较符号可能反转,导致查找走向错误分支。
11. TreeMap 的子映射视图与原 TreeMap 的关系?
标准回答:
TreeMap 的 subMap、headMap、tailMap 返回的子视图(如 AscendingSubMap)并不是独立的数据副本,而是底层红黑树的“受限窗口”。视图内部持有对原 TreeMap 的引用和边界范围(from, to, inclusive 标志)。任何对视图的操作(get、put、remove、迭代)都会委托给原 TreeMap 的对应方法,并在操作前或后施加边界检查。
- 修改互见:在视图中插入或删除元素,会直接反映在原 TreeMap 中;反之,原 TreeMap 的结构变化也会立刻在视图的后续操作中体现。
- 边界抛异常:向视图中插入键时,如果键不在
[from, to)范围(根据 inclusive 决定是否包含端点),会抛出IllegalArgumentException。 - 视图可序列化:视图本身也实现了
NavigableMap,可以继续进行导航和子映射操作。
追问模拟:
-
追问:如果原 TreeMap 中插入了一个在视图范围内的元素,视图能迭代到吗?
回答:可以。因为视图的迭代器通过调用原 TreeMap 的遍历方法并内部过滤,动态反映当前树的实际内容。 -
追问:修改子视图的结构,原 TreeMap 的 modCount 会变吗?
回答:会。因为子视图的 put/remove 最终调用原 TreeMap 的方法,这些方法会增加modCount,从而影响所有关联迭代器的 fail-fast 行为。
加分回答:
- 源码结构:
AscendingSubMap继承自NavigableSubMap,其put方法大致为:
public V put(K key, V value) {
if (!inRange(key))
throw new IllegalArgumentException("key out of range");
return m.put(key, value);
}
- 内存效率:由于共享同一棵树,无论创建多少子视图,都不会复制数据。但需要注意,子视图会持有原 TreeMap 的引用,导致原 TreeMap 无法被 GC,生命周期与视图绑定。
- 典型应用:有效分页或按区间查找,如
map.subMap(0, true, 100, true)可快速获取分数在 0~100 的用户,而无需遍历整个映射。