JDK 8 中的 TreeMap
是基于红黑树(Red-Black Tree)实现的 有序映射表,它继承自 AbstractMap
并实现了 NavigableMap
接口(NavigableMap
继承自 SortedMap
)。TreeMap
的核心特性是按键的自然顺序或自定义比较器排序,并支持高效的导航操作(如查找最接近的键、范围查询等)。
一、TreeMap 的核心特性
特性 | 说明 |
---|---|
有序性 | 键(Key)按自然顺序(若键实现 Comparable )或自定义 Comparator 排序。 |
时间复杂度 | 插入、删除、查找等操作的平均/最坏时间复杂度为 O(log n) (红黑树的高度为 log n )。 |
不允许重复键 | 键唯一(若插入相同键,新值覆盖旧值)。 |
不允许 null 键 | 若使用自然排序(无 Comparator ),键必须非 null (因 Comparable 不允许 null );若使用自定义 Comparator ,可能允许 null (但需 Comparator 处理 null )。 |
导航方法 | 支持 ceilingKey 、floorKey 、higherKey 、lowerKey 等方法,快速查找最接近的键。 |
二、TreeMap 的核心结构
1. 继承与接口
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
NavigableMap
:扩展了SortedMap
,增加了导航方法(如查找最近键)和视图操作(如子映射)。AbstractMap
:提供Map
接口的骨架实现,减少重复代码。
2. 核心属性
// 红黑树的根节点
private transient Entry<K,V> root;
// 元素数量
private transient int size = 0;
// 修改次数(用于快速失败迭代器)
private transient int modCount = 0;
// 比较器(若为 null,使用键的自然排序)
private final Comparator<? super K> comparator;
// 序列化版本号
private static final long serialVersionUID = 9172710721974048732L;
3. 红黑树节点(Entry)
TreeMap
的内部类 Entry
继承了 HashMap.Node
(JDK 8 中 HashMap
的节点类),但扩展了红黑树所需的属性:
static final class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> left; // 左子节点
Entry<K,V> right; // 右子节点
Entry<K,V> parent; // 父节点
boolean color = BLACK; // 节点颜色(默认黑色)
// 构造方法(与 HashMap.Node 兼容)
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
- 红黑树节点颜色:
color
字段标记节点颜色(RED
或BLACK
),默认新节点为红色(避免破坏黑高)。 - 父/子节点指针:通过
parent
、left
、right
指针连接,形成树结构。
三、构造方法
TreeMap
提供了多种构造方法,核心是初始化比较器和根节点:
1. 默认构造方法(自然排序)
public TreeMap() {
comparator = null; // 使用键的自然排序
}
2. 指定比较器的构造方法
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator; // 使用自定义比较器
}
3. 从 Map 初始化(按键排序)
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m); // 插入所有键值对,按键排序
}
4. 从 SortedMap 初始化(保留排序)
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator(); // 继承原 SortedMap 的比较器
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
buildFromSorted
是优化方法,用于从已排序的集合高效构建红黑树(类似归并排序的线性时间构建)。
四、核心方法解析
1. put(K key, V value):插入键值对
put
方法的核心逻辑是:找到键的插入位置,插入新节点,然后调整红黑树以保持平衡。
步骤 1:检查键是否合法
若使用自然排序(comparator == null
),键必须非 null
(否则抛出 NullPointerException
);若使用自定义比较器,comparator.compare(key, key)
不能返回 0
(否则视为重复键?实际取决于比较器实现)。
步骤 2:找到插入位置
通过 compare
方法比较键的大小,从根节点开始遍历,找到合适的父节点:
private Entry<K,V> putTreeVal(int h, K k, V v, boolean animate) {
Entry<K,V> parent; // 父节点
Entry<K,V> p; // 当前遍历节点
int cmp; // 比较结果
Entry<K,V> t = root;
// 根节点为空,直接创建新节点作为根
if (t == null) {
root = new Entry<>(h, k, v, null);
size = 1;
modCount++;
return null;
}
// 遍历树,找到插入位置
while (t != null) {
parent = t;
cmp = compare(k, t.key); // 比较当前键与节点键
if (cmp < 0) {
t = t.left; // 键更小,向左子树
} else if (cmp > 0) {
t = t.right; // 键更大,向右子树
} else {
// 键已存在,替换值并返回旧值
V oldValue = t.value;
t.value = v;
return oldValue;
}
}
// 找到父节点,创建新节点
Entry<K,V> e = new Entry<>(h, k, v, parent);
if (cmp < 0) {
parent.left = e;
} else {
parent.right = e;
}
// 插入后调整红黑树
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
步骤 3:调整红黑树(fixAfterInsertion)
插入新节点后,可能破坏红黑树的平衡性质(如连续两个红色节点)。fixAfterInsertion
通过变色和旋转修复平衡:
-
红黑树性质:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 所有叶子节点(
NIL
)是黑色。 - 红色节点的两个子节点都是黑色(无连续红色)。
- 从任一节点到其叶子的所有路径包含相同数量的黑色节点(黑高相同)。
-
插入修复逻辑:
新插入的节点默认是红色(避免破坏黑高)。若父节点是黑色,无需调整;若父节点是红色,则违反性质4,需根据叔叔节点的颜色调整:- 叔叔节点是红色:将父、叔节点设为黑色,祖父节点设为红色,向上递归检查祖父节点。
- 叔叔节点是黑色(或
NIL
):通过旋转(左旋/右旋)和变色修复。
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED; // 新节点默认红色
// 循环直到父节点是黑色或到达根节点
while (x != null && 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是父的右子节点(需左旋)
x = parentOf(x);
rotateLeft(x); // 左旋父节点
}
setColor(parentOf(x), BLACK); // 父变黑
setColor(parentOf(parentOf(x)), RED); // 祖父变红
rotateRight(parentOf(parentOf(x))); // 右旋祖父
}
} else { // 父节点是祖父的右子节点(对称逻辑)
Entry<K,V> y = leftOf(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 == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x); // 右旋父节点
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x))); // 左旋祖父
}
}
}
root.color = BLACK; // 根节点始终黑色
}
2. get(Object key):查找值
get
方法通过比较键的大小,在红黑树中快速定位目标节点:
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p == null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// 根据比较器或自然排序查找
if (comparator != null) {
return getUsingComparator(key);
} else {
if (key == null) {
throw new NullPointerException();
}
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0) {
p = p.left;
} else if (cmp > 0) {
p = p.right;
} else {
return p;
}
}
return null;
}
}
3. remove(Object key):删除键值对
remove
方法的核心是:找到要删除的节点,用后继节点替换它,然后调整红黑树。
步骤 1:找到待删除节点
通过 getEntry
找到目标节点 p
,若不存在则返回。
步骤 2:确定替换节点
- 若
p
无左子节点,用右子节点替换; - 若
p
无右子节点,用左子节点替换; - 若
p
有左右子节点,用**右子树的最小节点(后继)**替换p
。
步骤 3:调整红黑树
删除节点后,可能破坏红黑树性质(如黑高变化),需通过 fixAfterDeletion
调整。
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null) {
return null;
}
V oldValue = p.value;
deleteEntry(p); // 删除节点并调整树
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// 确定替换节点(successor)
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
// 情况 1:替换节点存在(有左或右子节点)
if (replacement != null) {
replacement.parent = p.parent; // 替换节点的父指向 p 的父
if (p.parent == null) {
root = replacement; // p 是根节点,替换为根
} else if (p == p.parent.left) {
p.parent.left = replacement; // 替换为左子节点
} else {
p.parent.right = replacement; // 替换为右子节点
}
// 若 p 是被删除的节点(非替换节点),复制其值到 p(后续调整用)
p.left = p.right = p.parent = null;
if (p.color == BLACK) {
fixAfterDeletion(replacement); // 调整红黑树
}
}
// 情况 2:替换节点不存在(p 是叶子节点)
else if (p.parent == null) {
root = null; // 树为空
}
// 情况 3:p 是根节点且有子节点(但子节点无后代)
else {
if (p.color == BLACK) {
fixAfterDeletion(p); // 调整红黑树
}
if (p.parent != null) {
if (p == p.parent.left) {
p.parent.left = null;
} else if (p == p.parent.right) {
p.parent.right = null;
}
p.parent = null;
}
}
}
步骤 4:调整红黑树(fixAfterDeletion)
删除黑色节点会破坏黑高性质(路径黑高减少),需通过变色和旋转修复:
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) { // x 是左子节点
Entry<K,V> s = rightOf(parentOf(x)); // 兄弟节点
if (colorOf(s) == RED) { // 兄弟是红色
setColor(s, BLACK); // 兄变黑
setColor(parentOf(x), RED); // 父变红
rotateLeft(parentOf(x)); // 左旋父节点
s = rightOf(parentOf(x)); // 更新兄弟节点
}
if (colorOf(leftOf(s)) == BLACK && colorOf(rightOf(s)) == BLACK) { // 兄弟的子节点都是黑色
setColor(s, RED); // 兄变红
x = parentOf(x); // 继续向上检查
} else {
if (colorOf(rightOf(s)) == BLACK) { // 兄的右子节点是黑色
setColor(leftOf(s), BLACK); // 兄的左子节点变黑
setColor(s, RED); // 兄变红
rotateRight(s); // 右旋兄节点
s = rightOf(parentOf(x)); // 更新兄弟节点
}
setColor(s, colorOf(parentOf(x))); // 兄继承父的颜色
setColor(parentOf(x), BLACK); // 父变黑
setColor(rightOf(s), BLACK); // 兄的右子节点变黑
rotateLeft(parentOf(x)); // 左旋父节点
x = root; // 退出循环
}
} else { // x 是右子节点(对称逻辑)
Entry<K,V> s = leftOf(parentOf(x));
if (colorOf(s) == RED) {
setColor(s, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
s = leftOf(parentOf(x));
}
if (colorOf(rightOf(s)) == BLACK && colorOf(leftOf(s)) == BLACK) {
setColor(s, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(s)) == BLACK) {
setColor(rightOf(s), BLACK);
setColor(s, RED);
rotateLeft(s);
s = leftOf(parentOf(x));
}
setColor(s, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(s), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}
setColor(x, BLACK); // 最终 x 设为黑色(可能是根节点)
}
五、导航方法(NavigableMap 接口)
TreeMap
实现了 NavigableMap
,提供了以下关键方法:
1. ceilingKey(K key):查找大于等于 key 的最小键
public K ceilingKey(K key) {
return keyOrNull(ceilingEntry(key));
}
final Entry<K,V> ceilingEntry(K key) {
return getCeilingEntry(key); // 内部实现
}
2. floorKey(K key):查找小于等于 key 的最大键
public K floorKey(K key) {
return keyOrNull(floorEntry(key));
}
3. higherKey(K key):查找大于 key 的最小键
public K higherKey(K key) {
return keyOrNull(higherEntry(key));
}
4. lowerKey(K key):查找小于 key 的最大键
public K lowerKey(K key) {
return keyOrNull(lowerEntry(key));
}
5. subMap(K fromKey, K toKey):返回键在 [fromKey, toKey) 的子映射
public NavigableMap<K,V> subMap(K fromKey, K toKey) {
return new SubMap<>(this, fromKey, true, toKey, false);
}
- 子映射通过
SubMap
内部类实现,共享原树的节点,通过视图机制实现高效操作。
六、关键源码细节
1. 红黑树的旋转操作
红黑树通过左旋和右旋调整结构,保持平衡:
// 左旋节点 x(x 的右子节点 y 成为新的父节点)
private void rotateLeft(Entry<K,V> x) {
Entry<K,V> y = x.right;
x.right = y.left; // y 的左子树成为 x 的右子树
if (y.left != null) {
y.left.parent = x; // 更新 y 左子节点的父
}
y.parent = x.parent; // y 的父指向 x 的父
if (x.parent == null) {
root = y; // x 是根,更新根为 y
} else if (x == x.parent.left) {
x.parent.left = y; // x 是左子节点,更新父的左子节点为 y
} else {
x.parent.right = y; // x 是右子节点,更新父的右子节点为 y
}
y.left = x; // x 成为 y 的左子节点
x.parent = y; // x 的父指向 y
}
// 右旋节点 y(y 的左子节点 x 成为新的父节点)
private void rotateRight(Entry<K,V> y) {
Entry<K,V> x = y.left;
y.left = x.right; // x 的右子树成为 y 的左子树
if (x.right != null) {
x.right.parent = y; // 更新 x 右子节点的父
}
x.parent = y.parent; // x 的父指向 y 的父
if (y.parent == null) {
root = x; // y 是根,更新根为 x
} else if (y == y.parent.right) {
y.parent.right = x; // y 是右子节点,更新父的右子节点为 x
} else {
y.parent.left = x; // y 是左子节点,更新父的左子节点为 x
}
x.right = y; // y 成为 x 的右子节点
y.parent = x; // y 的父指向 x
}
2. 比较逻辑
TreeMap
使用 compare
方法比较键的大小,优先使用自定义比较器,否则使用键的自然排序:
private int compare(Object k1, Object k2) {
return (comparator != null) ? comparator.compare(k1, k2) : ((Comparable<? super K>)k1).compareTo(k2);
}
七、总结
TreeMap
是基于红黑树的有序映射,核心优势是按键排序和高效的导航操作。其内部通过红黑树维护平衡,保证了插入、删除、查找的 O(log n)
时间复杂度。实际开发中,若需要有序性和导航功能,TreeMap
是首选;若只需快速查找且不要求顺序,HashMap
更高效(平均 O(1)
时间复杂度)。