集合-TreeSet&TreeMap源码
大约 10 分钟
集合-TreeSet&TreeMap源码
一、核理论
1.1 TreeMap与TreeSet类结构分析
TreeMap是基于红黑树实现的有序键值对存储结构,而TreeSet则是基于TreeMap实现的有序集合,用于存储不重复元素。两者都实现了Navigable接口,支持一系列导航方法,默认按自然顺序排序或通过Comparator自定义排序。
1.2 核心成员变量
TreeMap核心变量
root: 红黑树的根节点size: 键值对数量comparator: 用于排序的比较器,null表示自然排序modCount: 结构修改计数器,用于快速失败机制
TreeSet核心变量
m: 内部使用的NavigableMap实例(实际为TreeMap)PRESENT: 静态常量,作为Map中的value占位符
1.3 红黑树特性
红黑树是一种自平衡的二叉查找树,具有以下特性:
- 每个节点要么是红色,要么是黑色
- 根节点是黑色
- 所有叶子节点(NIL)是黑色
- 如果一个节点是红色,则它的两个子节点都是黑色
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
这些特性确保了红黑树的平衡,使得基本操作(插入、删除、查找)的时间复杂度为O(log n)。
1.4 JDK版本特性差异
- JDK 1.2: 引入TreeMap和TreeSet
- JDK 1.6: 引入NavigableMap和NavigableSet接口,TreeMap和TreeSet实现这些接口
- JDK 1.8: 优化红黑树实现,提高并发性能
- JDK 1.9: 引入of()静态工厂方法创建不可变SortedMap
- JDK 16: 增强NavigableMap接口,添加更多便捷方法
二、代码实践
2.1 TreeMap核心方法实现
2.1.1 红黑树节点结构
/**
* 红黑树节点
*/
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; // 默认黑色
// 构造方法和其他方法...
}2.1.2 put方法实现
/**
* 将键值对放入映射
* @param key 键
* @param value 值
* @return 之前与key关联的值,或null
*/
public V put(K key, V value) {
Entry<K,V> t = root;
// 树为空,创建根节点
if (t == null) {
compare(key, key); // 检查key是否为null(自然排序时)
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// 分裂比较器和比较逻辑
Comparator<? super K> cpr = comparator;
if (cpr != null) {
// 使用自定义比较器查找插入位置
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else // 键已存在,替换值
return t.setValue(value);
} while (t != null);
}
else {
// 使用自然排序
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else // 键已存在,替换值
return t.setValue(value);
} while (t != null);
}
// 创建新节点
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 插入后修复红黑树平衡
fixAfterInsertion(e);
size++;
modCount++;
return null;
}2.1.3 红黑树插入后修复
/**
* 插入后修复红黑树平衡
*/
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) {
// 情况1:叔叔节点为红色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
// 情况2:叔叔节点为黑色,且当前节点为右孩子
x = parentOf(x);
rotateLeft(x);
}
// 情况3:叔叔节点为黑色,且当前节点为左孩子
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) {
// 情况1:叔叔节点为红色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
// 情况2:叔叔节点为黑色,且当前节点为左孩子
x = parentOf(x);
rotateRight(x);
}
// 情况3:叔叔节点为黑色,且当前节点为右孩子
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
x = parentOf(x);
}
// 确保根节点为黑色
root.color = BLACK;
}2.1.4 旋转操作
/**
* 左旋操作
*/
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}
/**
* 右旋操作
*/
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left;
p.left = l.right;
if (l.right != null)
l.right.parent = p;
l.parent = p.parent;
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else
p.parent.left = l;
l.right = p;
p.parent = l;
}
}2.2 TreeSet核心方法实现
/**
* 向集合中添加元素
* @param e 要添加的元素
* @return 如果元素不存在且添加成功则返回true
*/
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
/**
* 获取集合的迭代器
* @return 按升序排列的元素迭代器
*/
public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}
/**
* 获取小于指定元素的最大元素
* @param e 要比较的元素
* @return 小于e的最大元素,不存在则返回null
*/
public E lower(E e) {
return m.lowerKey(e);
}
/**
* 获取大于指定元素的最小元素
* @param e 要比较的元素
* @return 大于e的最小元素,不存在则返回null
*/
public E higher(E e) {
return m.higherKey(e);
}2.3 实际应用示例
2.3.1 自定义排序示例
/**
* 使用自定义比较器的TreeMap示例
* 按字符串长度排序,长度相同则按字母顺序
*/
public class CustomComparatorExample {
public static void main(String[] args) {
// 创建自定义比较器
Comparator<String> lengthComparator = (s1, s2) -> {
if (s1.length() != s2.length()) {
return Integer.compare(s1.length(), s2.length());
} else {
return s1.compareTo(s2);
}
};
// 使用自定义比较器创建TreeMap
Map<String, Integer> treeMap = new TreeMap<>(lengthComparator);
// 添加元素
treeMap.put("apple", 1);
treeMap.put("banana", 2);
treeMap.put("pear", 3);
treeMap.put("orange", 4);
treeMap.put("grape", 5);
// 输出将按字符串长度排序
for (Map.Entry<String, Integer> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出顺序: pear(4), apple(5), grape(5), banana(6), orange(6)
}
}2.3.2 导航方法使用示例
/**
* TreeSet导航方法使用示例
*/
public class TreeSetNavigationExample {
public static void main(String[] args) {
TreeSet<Integer> numbers = new TreeSet<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
numbers.add(50);
System.out.println("集合: " + numbers); // [10, 20, 30, 40, 50]
System.out.println("第一个元素: " + numbers.first()); // 10
System.out.println("最后一个元素: " + numbers.last()); // 50
System.out.println("小于30的最大元素: " + numbers.lower(30)); // 20
System.out.println("大于30的最小元素: " + numbers.higher(30)); // 40
System.out.println("小于等于30的最大元素: " + numbers.floor(30)); // 30
System.out.println("大于等于30的最小元素: " + numbers.ceiling(30)); // 30
// 子集视图
SortedSet<Integer> subset = numbers.subSet(20, 40);
System.out.println("20到40的子集: " + subset); // [20, 30]
}
}三、设计思想
3.1 红黑树数据结构
TreeMap采用红黑树作为底层数据结构,具有以下优势:
- 自平衡:通过旋转和变色操作保持树的平衡
- 有序性:中序遍历可得到有序序列
- 高效性:插入、删除、查找操作的时间复杂度均为O(log n)
3.2 排序机制
TreeMap支持两种排序方式:
- 自然排序:键必须实现Comparable接口,通过compareTo()方法比较
- 定制排序:创建TreeMap时提供Comparator对象,通过compare()方法比较
排序机制确保了TreeMap中的键始终处于有序状态,支持高效的范围查询操作。
3.3 视图机制
TreeMap提供了多种视图:
- keySet():返回键的Set视图
- values():返回值的Collection视图
- entrySet():返回键值对的Set视图
- navigableKeySet():返回支持导航方法的键集视图
- descendingKeySet():返回逆序的键集视图
这些视图是动态的,对视图的修改会反映到底层Map,反之亦然。
3.4 与HashMap的对比
| 特性 | TreeMap | HashMap |
|---|---|---|
| 数据结构 | 红黑树 | 数组+链表+红黑树 |
| 有序性 | 按键有序 | 无序 |
| 时间复杂度 | O(log n) | 平均O(1),最坏O(n) |
| 排序方式 | 自然排序/定制排序 | 无 |
| 导航方法 | 支持 | 不支持 |
| 哈希依赖 | 不依赖 | 依赖hashCode()和equals() |
四、避坑指南
4.1 键的可比较性
使用自然排序时,键必须实现Comparable接口,否则会抛出ClassCastException:
// 错误示例:使用不可比较的对象作为键
class UncomparableKey {
private int value;
// 省略构造方法和getter
}
Map<UncomparableKey, String> map = new TreeMap<>();
map.put(new UncomparableKey(1), "value"); // 抛出ClassCastException4.2 键的不可变性
作为TreeMap键的对象应保持不可变,否则修改键可能导致排序混乱:
// 错误示例:使用可变对象作为键
class MutableKey implements Comparable<MutableKey> {
private int value;
// 省略构造方法和getter
@Override
public int compareTo(MutableKey o) {
return Integer.compare(this.value, o.value);
}
}
TreeMap<MutableKey, String> map = new TreeMap<>();
MutableKey key = new MutableKey(1);
map.put(key, "value");
key.setValue(2); // 修改键的值
String value = map.get(key); // 可能为null或错误值4.3 线程安全问题
TreeMap是非线程安全的,多线程环境下可能导致:
- 数据不一致
- ConcurrentModificationException异常
解决方案:
// 1. 使用Collections.synchronizedSortedMap
SortedMap<String, Integer> syncMap = Collections.synchronizedSortedMap(new TreeMap<>());
// 2. 使用并发工具类加锁
Map<String, Integer> concurrentMap = new TreeMap<>();
// ... 使用ReentrantLock或synchronized块保护访问 ...4.4 性能陷阱
- 频繁插入删除:红黑树的旋转和变色操作开销较大,频繁修改可能影响性能
- 错误的比较器实现:比较器违反compareTo()约定(如不对称、不传递)会导致异常行为
- 范围查询效率:TreeMap的范围查询(如subMap())效率远高于HashMap
五、深度思考题
- TreeMap中的红黑树与HashMap中的红黑树有何异同?为什么HashMap在JDK 8中才引入红黑树?
- 如何实现一个线程安全的TreeMap?ConcurrentSkipListMap与TreeMap相比有哪些优势和劣势?
- TreeMap的subMap()方法返回的视图与原Map是什么关系?修改视图会对原Map产生什么影响?
- 比较TreeSet、HashSet和LinkedHashSet的性能特性,分析各自适用的场景。
- 如何在TreeMap中实现自定义排序,同时保证equals()方法与compareTo()方法的一致性?
思考题回答:
TreeMap与HashMap中红黑树的异同:
- 相同点: 都实现了自平衡二叉查找树特性,保证O(log n)时间复杂度
- 不同点: TreeMap的红黑树用于维护键的有序性,是其唯一数据结构;HashMap的红黑树仅在链表长度超过阈值时使用,用于解决哈希冲突
- HashMap在JDK 8引入红黑树的原因: 提高哈希冲突严重时的查询性能,从O(n)提升到O(log n)
线程安全的TreeMap实现方式:
- 使用Collections.synchronizedSortedMap包装
- 使用ConcurrentSkipListMap (JDK 6+)
- ConcurrentSkipListMap优势: 无锁设计,支持更高并发;劣势: 内存占用更大,随机访问性能略低于TreeMap
