TreeMap | JavaSE
TreeMap
一、学习目标
完成本章后,你应该能够:
- 能够解释
TreeMap的核心特点:键不重复、无索引、按照键进行排序。 - 能够说明
TreeMap与HashMap、LinkedHashMap在数据结构、顺序特征和应用场景上的区别。 - 能够使用键的自然排序(Natural Ordering)和
Comparator两种方式控制 TreeMap 的排序规则。 - 能够解释 TreeMap 为什么根据键的比较结果而不是 value 判断键是否重复。
- 能够理解 TreeMap 基于红黑树实现,并解释其
put/get/remove/containsKey的典型时间复杂度。 - 能够使用 TreeMap 完成词频排序、成绩统计、排行榜、区间查询等实际任务。
二、核心知识
2.1 TreeMap 是什么
TreeMap<K, V> 是 Java 集合框架中 Map<K,V> 的一个重要实现类。
它最核心的特点是:
键不重复
+
按照键排序
+
无索引
例如:
import java.util.Map;
import java.util.TreeMap;
public class TreeMapDemo {
public static void main(String[] args) {
Map<Integer, String> map = new TreeMap<>();
map.put(30, "Java");
map.put(10, "C");
map.put(40, "Python");
map.put(20, "C++");
System.out.println(map);
}
}
虽然添加顺序是:
30
10
40
20
TreeMap 会按照键的自然顺序组织:
10
20
30
40
因此可以建立第一条核心认识:
TreeMap 排序的是 key,不是 value。
2.2 TreeMap 在 Map 体系中的位置
前面已经学习:
Map<K,V>
├── HashMap
├── LinkedHashMap
└── TreeMap
三者共同具有:
key → value
这种键值对数据模型。
但是 key 的组织方式不同。
| Map 实现 | 键顺序 | 主要底层结构 | 典型用途 |
| --------------- | -------------------- | ----------------- | ------------------------ |
| HashMap | 不保证顺序 | 哈希表 | 高频增删查、普通键值映射 |
| LinkedHashMap | 可维护确定的迭代顺序 | 哈希表 + 双向链表 | 既要哈希性能又要顺序 |
| TreeMap | 按键排序 | 红黑树 | 需要有序键、范围查询 |
因此选择 TreeMap 的核心条件通常不是:
“我要存键值对。”
因为所有 Map 都可以。
而是:
我要存键值对,并且需要 key 始终保持某种排序关系。
2.3 TreeMap 只按照 key 排序
假设:
TreeMap<String, Integer> map = new TreeMap<>();
map.put("Java", 95);
map.put("C", 100);
map.put("Python", 60);
TreeMap 不会因为:
100 > 95 > 60
就按照 value 排列。
它真正比较的是:
"C"
"Java"
"Python"
所以:
TreeMap<K,V>
↑
排序 K
而不是:
TreeMap<K,V>
↑
排序 V
如果业务要求:
“按照 value 排序”
通常需要把 Map 数据转换为其他结构再排序,例如:
entrySet
→ List<Entry<K,V>>
→ Comparator 按 value 排序
或者根据实际业务重新设计数据模型。
2.4 TreeMap 的键为什么不能重复
普通 Map 已经规定:
一个 key
只能对应一个 value
TreeMap 同样如此。
但是 TreeMap 判断两个 key 是否属于同一个排序位置时,核心依赖:
compareTo()
或者:
Comparator.compare()
假设比较结果:
< 0
→ 第一个 key 更小
> 0
→ 第一个 key 更大
== 0
→ TreeMap 认为两个 key 在排序意义上相等
因此:
对 TreeMap 来说,比较结果为
0非常重要。
例如:
TreeMap<Integer, String> map = new TreeMap<>();
map.put(10, "Java");
map.put(10, "Python");
第二次 put 使用相同 key:
10
因此会覆盖原来的 value。
最后相当于:
10 → Python
2.5 TreeMap 的两种排序方式
TreeMap 与之前学习的 TreeSet 很相似。
主要有两套排序方案:
方案一
Comparable
→ key 自己定义自然排序规则
方案二
Comparator
→ 创建 TreeMap 时提供外部排序规则
可以理解为:
Comparable
= 我这个类天生应该怎么排
Comparator
= 这一次我想怎么排
2.6 自然排序
对于 Java 已经实现 Comparable 的类型,例如:
Integer
String
Long
Double
...
可以直接:
TreeMap<Integer, String> map = new TreeMap<>();
Integer 已经拥有自然排序规则,因此 TreeMap 可以直接比较 key。
例如:
map.put(30, "A");
map.put(10, "B");
map.put(20, "C");
按照 Integer 的自然顺序:
10
20
30
2.7 自定义对象作为 key
假设:
public class Student {
private int id;
private String name;
}
如果直接:
TreeMap<Student, Integer> map = new TreeMap<>();
TreeMap 必须知道:
两个 Student 到底谁大谁小?
如果 Student 没有实现 Comparable<Student>,同时 TreeMap 又没有提供 Comparator,那么就没有排序规则。
一种解决方案是让 Student 自己实现:
Comparable<Student>
例如:
public class Student implements Comparable<Student> {
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.id, other.id);
}
@Override
public String toString() {
return "Student{id=%d, name='%s'}".formatted(id, name);
}
}
现在:
TreeMap<Student, Integer> map = new TreeMap<>();
就可以根据:
student.id
排序。
2.8 使用 Comparator 自定义排序
如果不想修改 Student 类本身,可以:
TreeMap<Student, Integer> map =
new TreeMap<>(
(s1, s2) -> Integer.compare(s1.getId(), s2.getId())
);
如果想倒序:
TreeMap<Student, Integer> map =
new TreeMap<>(
(s1, s2) -> Integer.compare(s2.getId(), s1.getId())
);
注意两个方向:
Integer.compare(s1.getId(), s2.getId())
通常表示:
id 小 → id 大
而:
Integer.compare(s2.getId(), s1.getId())
则将方向反过来:
id 大 → id 小
2.9 多条件排序
实际开发中经常不是只比较一个字段。
例如:
先按工资降序,如果工资相同,再按年龄升序,如果仍相同,再按姓名排序。
可以使用 Comparator 链:
Comparator<Teacher> comparator =
Comparator.comparingDouble(Teacher::getSalary)
.reversed()
.thenComparingInt(Teacher::getAge)
.thenComparing(Teacher::getName);
TreeMap<Teacher, String> map = new TreeMap<>(comparator);
这个例子非常重要。
因为如果只写:
(o1, o2) ->
Double.compare(o2.getSalary(), o1.getSalary())
那么:
Teacher A:salary = 10000
Teacher B:salary = 10000
比较结果:
0
TreeMap 就会把二者视为相同排序键。
如果业务上它们实际上是两个不同教师,就可能出现 value 被覆盖。
因此排序规则必须同时回答两个问题:
1. 谁排在谁前面?
2. 什么情况下两个 key 被视为相等?
第二个问题尤其重要。
三、使用方法
3.1 创建 TreeMap
自然排序:
Map<Integer, String> map = new TreeMap<>();
如果后面需要 TreeMap 特有 API,可以写:
TreeMap<Integer, String> map = new TreeMap<>();
3.2 添加键值对
仍然使用 Map 的:
put(K key, V value)
例如:
TreeMap<Integer, String> map = new TreeMap<>();
map.put(3, "Java");
map.put(1, "C");
map.put(4, "Python");
map.put(2, "C++");
TreeMap 会根据 key 自动维护排序关系。
3.3 获取元素
普通 Map API 依然可以使用:
map.get(key);
map.containsKey(key);
map.containsValue(value);
map.remove(key);
map.size();
map.isEmpty();
例如:
System.out.println(map.get(2));
System.out.println(map.containsKey(3));
TreeMap 仍然是 Map。
排序能力是在 Map 基础上的增强,而不是完全另一套集合。
3.4 遍历 TreeMap
可以继续使用 entrySet():
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
因为 TreeMap 的 key 本身有序,因此遍历结果会按照 TreeMap 的比较规则进行。
也可以:
map.forEach((key, value) ->
System.out.println(key + "=" + value));
3.5 获取第一个和最后一个 key
TreeMap 还具有排序 Map 特有的能力。
例如:
TreeMap<Integer, String> scores = new TreeMap<>();
scores.put(60, "及格");
scores.put(80, "良好");
scores.put(90, "优秀");
System.out.println(scores.firstKey());
System.out.println(scores.lastKey());
可以直接获得:
最小 key
最大 key
这体现了 TreeMap 与 HashMap 的本质差异:
HashMap
重点:快速映射
TreeMap
重点:有序映射
3.6 floor、ceiling、lower、higher
TreeMap 还支持有序查询。
假设:
10
20
30
40
查询:
map.floorKey(25);
含义:
找到
<= 25的最大 key。
结果:
20
而:
map.ceilingKey(25);
表示:
找到
>= 25的最小 key。
结果:
30
对应:
| 方法 | 含义 |
| --------------- | --------------------- |
| lowerKey(k) | 严格小于 k 的最大 key |
| floorKey(k) | 小于等于 k 的最大 key |
| ceilingKey(k) | 大于等于 k 的最小 key |
| higherKey(k) | 严格大于 k 的最小 key |
这类 API 非常适合:
- 时间区间
- 分数区间
- 价格区间
- 版本号映射
- 阶梯规则
3.7 获取一段范围
例如:
TreeMap<Integer, String> map = new TreeMap<>();
map.put(10, "A");
map.put(20, "B");
map.put(30, "C");
map.put(40, "D");
map.put(50, "E");
System.out.println(map.subMap(20, true, 40, true));
可以得到指定 key 区间。
还包括:
headMap(...)
tailMap(...)
subMap(...)
因此 TreeMap 不只是:
“一个会自动排序的 HashMap”。
它本质上还是一个:
SortedMap / NavigableMap。
四、原理与进阶
4.1 TreeMap 底层是红黑树
TreeMap 的核心底层数据结构是:
Red-Black Tree
红黑树
它属于一种:
自平衡二叉搜索树
可以抽象理解:
30
/ \
20 50
/ \ / \
10 25 40 60
因为树节点按照比较规则组织,所以可以进行:
左边较小
当前节点
右边较大
这样的定向查找。
4.2 为什么不用普通二叉搜索树
普通二叉搜索树如果输入:
1
2
3
4
5
6
可能退化成:
1
\
2
\
3
\
4
\
5
最终接近链表。
查找性能会变差。
红黑树通过旋转和变色等机制维持近似平衡,使树高保持在合理范围。
因此 TreeMap 的主要操作可以维持:
O(log n)
级别。
4.3 TreeMap 典型时间复杂度
JDK 21 的 TreeMap 对以下操作提供对数级时间保证:
put
get
remove
containsKey
典型复杂度:
O(log n)
与 HashMap 形成鲜明对比:
HashMap
平均情况下:
O(1)
TreeMap:
O(log n)
所以:
如果不需要排序,通常没必要仅仅为了“高级”而使用 TreeMap。
HashMap 通常具有更好的普通键值查找性能。
TreeMap 的价值来自:
排序
+
范围查询
+
有序导航
4.4 TreeMap 如何判断两个 key 相同
这是本章最关键的原理之一。
对于 HashMap:
hashCode
+
equals
非常关键。
对于 TreeMap:
compareTo
或者
Comparator.compare
才是排序和定位 key 的核心。
假设:
Comparator<Student> comparator =
Comparator.comparingInt(Student::getAge);
然后:
Student("张三", 20)
Student("李四", 20)
比较:
20 vs 20
得到:
0
TreeMap 会从排序结构角度将这两个 key 视为相同。
因此:
TreeMap 中的“不重复”
不能简单理解成:
equals() 不相等就一定能同时存在
更准确的是:
如果 TreeMap 的比较规则认为两个 key 相等,即比较结果为 0,它们就是同一个排序键位置。
4.5 排序规则与 equals 最好保持一致
考虑:
Student a = new Student(1, "张三", 20);
Student b = new Student(2, "李四", 20);
假设:
equals
根据 id 比较
所以:
a.equals(b) == false
但 TreeMap Comparator:
Comparator.comparingInt(Student::getAge)
得到:
20 == 20
→ compare(a,b) == 0
于是出现:
equals 认为:
不是同一个对象
排序规则认为:
是同一个 key
这会让 SortedMap 的行为非常容易令人困惑。
因此通常建议:
TreeMap 的比较规则与对象的 equals 语义保持一致,或者至少明确知道为什么不一致。
4.6 TreeMap 能不能存 null key
默认自然排序:
new TreeMap<>()
通常不能直接使用:
null
作为 key。
因为自然排序需要执行比较,而 null 没有:
compareTo()
如果使用自定义 Comparator,并且 Comparator 明确支持 null,则可以设计允许 null 的排序规则。
例如思想上可以使用:
Comparator.nullsFirst(...)
但普通业务代码中,更重要的是:
不要依赖 TreeMap 的 null key 作为常规设计。
value 则可以为 null。
五、实践应用
5.1 词频统计后按字典序输出
假设统计:
java
python
java
c
python
java
使用 TreeMap:
TreeMap<String, Integer> countMap = new TreeMap<>();
for (String word : words) {
countMap.put(
word,
countMap.getOrDefault(word, 0) + 1
);
}
最终:
c=1
java=3
python=2
统计结果天然按照 key 顺序输出。
5.2 HashMap 统计 + TreeMap 排序
实际开发中也可以:
第一阶段:
HashMap
负责快速统计
第二阶段:
TreeMap
负责按 key 排序
例如:
Map<String, Integer> counter = new HashMap<>();
// 完成大量统计……
Map<String, Integer> sorted = new TreeMap<>(counter);
这种组合体现了一个重要工程思想:
不同集合负责不同职责。
不是一定只能“从头到尾用一种集合”。
5.3 分数等级匹配
例如规则:
0 → 不及格
60 → 及格
80 → 良好
90 → 优秀
可以:
TreeMap<Integer, String> levels = new TreeMap<>();
levels.put(0, "不及格");
levels.put(60, "及格");
levels.put(80, "良好");
levels.put(90, "优秀");
学生:
score = 87
可以利用:
levels.floorEntry(score)
找到:
80 → 良好
这种“阶梯规则”就是 TreeMap 很典型的应用。
5.4 时间版本映射
例如系统配置:
2026-01-01 → 配置 A
2026-04-01 → 配置 B
2026-08-01 → 配置 C
如果查询:
2026-06-15
可以寻找:
小于等于该时间的最新配置。
本质仍然是:
floorEntry
TreeMap 的有序性能够非常自然地解决这类问题。
六、常见问题
6.1 TreeMap 按 key 还是 value 排序?
只按照:
key
排序。
value 不决定 TreeMap 的结构位置。
6.2 TreeMap 是不是比 HashMap 更高级?
不是。
它们解决不同问题。
HashMap
→ 更关注普通键值映射性能
TreeMap
→ 更关注排序和范围查询
技术选型不存在简单的:
A 高级
B 低级
而应该问:
需求是什么?
6.3 TreeMap 为什么没有索引?
因为 TreeMap 的本质不是:
0
1
2
3
这样的线性位置模型。
它是:
key → value
并且按照 key 的比较关系建立树结构。
6.4 自定义 key 一定要实现 Comparable 吗?
不一定。
两种方式任选其一:
key 实现 Comparable
或者:
创建 TreeMap 时提供 Comparator
6.5 Comparator 和 Comparable 同时存在使用谁?
如果创建 TreeMap 时明确传入:
new TreeMap<>(comparator)
则使用这个 Comparator。
可以理解为:
TreeMap 自己指定的 Comparator
优先于
key 自己的自然排序
6.6 Comparator 返回 0 会发生什么?
TreeMap 会从排序意义上将两个 key 视为相同。
这可能导致:
后 put 的 value
覆盖前一个 value
因此 Comparator 绝不能只考虑“排序好看”,还必须考虑:
哪些对象允许被认为是相同 key?
6.7 为什么比较 double 不建议直接相减再强转 int?
例如不要写:
return (int) (o1.getSalary() - o2.getSalary());
因为可能出现:
- 小数精度问题
- 差值不足 1 被截断为 0
- 极端数值问题
应该使用:
Double.compare(a, b);
七、练习与验收
7.1 知识问答
- TreeMap 的三个核心特点是什么?
- TreeMap 按 key 还是 value 排序?
- TreeMap 的底层主要是什么数据结构?
- TreeMap 与 HashMap 的核心区别是什么?
- TreeMap 与 LinkedHashMap 的“有序”分别表示什么?
- TreeMap 有哪两种指定排序规则的方式?
- Comparable 与 Comparator 的职责分别是什么?
- Comparator 返回正数、负数和 0 分别表示什么?
- 为什么 Comparator 返回 0 会影响 TreeMap 的 key 去重?
- TreeMap 的
put/get/remove/containsKey典型复杂度是多少? - 为什么没有排序需求时通常优先考虑 HashMap?
- 什么情况下 TreeMap 的范围查询能力比 HashMap 更适合?
- 为什么排序规则最好与 equals 保持一致?
7.2 代码阅读
阅读:
TreeMap<Integer, String> map = new TreeMap<>();
map.put(30, "C");
map.put(10, "A");
map.put(20, "B");
map.put(20, "Java");
System.out.println(map);
回答:
- TreeMap 最终有几个键值对?
- key 的遍历顺序是什么?
20最终对应什么 value?- 为什么?
阅读:
TreeMap<Student, String> map =
new TreeMap<>(
(s1, s2) ->
Integer.compare(s1.getAge(), s2.getAge())
);
随后加入:
张三,20
李四,20
王五,21
回答:
- 三个 Student 是否一定都能作为独立 key 保存?
- 哪两个对象的 Comparator 结果可能是 0?
- 如何修改比较规则避免非预期覆盖?
7.3 手写代码
- 使用
TreeMap<Integer,String>保存学号和姓名,并按照学号升序输出。 - 改为学号降序。
- 定义 Teacher,按照 salary 降序排序。
- salary 相同时,再按照 age 升序。
- age 仍相同时,再按照 name 排序。
- 使用 TreeMap 完成字符串词频统计,并按单词字典序输出。
- 使用 TreeMap 的范围查询 API 完成分数等级匹配。
7.4 Debug
下面 Comparator 存在业务风险:
TreeMap<Teacher, String> map =
new TreeMap<>(
(t1, t2) ->
Double.compare(
t2.getSalary(),
t1.getSalary()
)
);
要求:
- 判断当前排序是升序还是降序。
- 如果两个 Teacher 的 salary 相同,会发生什么?
- 为什么这可能导致数据被覆盖?
- 增加第二、第三排序条件修复。
下面代码:
TreeMap<Student, String> map = new TreeMap<>();
map.put(new Student(1, "张三"), "A");
假设 Student 没有实现 Comparable。
要求:
- 分析问题产生原因。
- 给出 Comparable 解决方案。
- 给出 Comparator 解决方案。
7.5 综合训练
设计一个考试成绩等级系统。
要求:
0 → 不及格
60 → 及格
70 → 中等
80 → 良好
90 → 优秀
输入任意:
0~100
的成绩。
要求使用 TreeMap 的有序查询能力,判断该学生对应等级。
禁止通过:
if-else if
硬编码全部等级区间。
并思考:
- 为什么 TreeMap 非常适合阶梯配置?
- 如果增加一个新的
95 → 卓越,程序主体是否需要修改?
7.6 本章验收
- [ ] 能闭卷说出 TreeMap 的核心特点。
- [ ] 能说出 TreeMap、HashMap、LinkedHashMap 的选择区别。
- [ ] 能解释 TreeMap 为什么基于 key 排序。
- [ ] 能手写 Comparable 和 Comparator 两种 TreeMap 排序方式。
- [ ] 能解释 Comparator 返回 0 对 key 唯一性的影响。
- [ ] 能解释 TreeMap 红黑树与 O(log n) 的关系。
- [ ] 能使用 firstKey、lastKey、floorKey、ceilingKey。
- [ ] 能完成一个 TreeMap 范围查询案例。
- [ ] 能根据业务需求判断是否应该选择 TreeMap。