TreeMap 详解
本章定位:集合框架中专门解决"按键排序"需求的 Map 实现。TreeMap 基于红黑树(自平衡二叉搜索树),所有键严格按照自然顺序或自定义 Comparator 排列。虽然 get/put 的时间复杂度 O(log n) 不如 HashMap 的 O(1),但 TreeMap 提供了丰富的导航方法和范围查询能力。飞翔科技的工资排名、订单日期查询、项目进度排序等场景都依赖 TreeMap。本章由白歌带领小崔深入红黑树,大翔关注查询性能,Frank 负责订单业务模型设计。
定义速览表
| 属性 | 说明 |
|---|---|
| 底层数据结构 | 红黑树(Red-Black Tree,自平衡二叉搜索树) |
| 键的排序 | 自然顺序(元素实现 Comparable)或定制顺序(传入 Comparator) |
| 是否允许 null 键 | 不允许(插入时需调用 compareTo/compare,会 NPE) |
| 是否允许 null 值 | 允许(值不参与排序) |
| 线程安全 | 否 |
| 时间复杂度 | get/put/remove/containsKey 均为 O(log n) |
| 实现接口 | NavigableMap<K,V> → SortedMap<K,V> → Map<K,V> |
| fail-fast | 是(迭代器在结构性修改时抛 ConcurrentModificationException) |
| 排序稳定性 | 依赖于 Comparator/Comparable 实现(建议与 equals 一致) |
| 内存开销 | 每个 Entry 维护 parent、left、right、color 四个引用,比 HashMap 节点大 |
接口继承体系
方法速查表
基础 Map 操作
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
put(K key, V value) | 插入键值对,按键排序存储 | O(log n) |
get(Object key) | 根据键获取值,沿树查找 | O(log n) |
remove(Object key) | 删除键值对,之后重新平衡 | O(log n) |
containsKey(Object key) | 判断是否包含指定键 | O(log n) |
containsValue(Object value) | 判断是否包含指定值 | O(n)(需遍历) |
size() | 返回键值对数量 | O(1) |
导航方法(NavigableMap 接口)
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
firstKey() / firstEntry() | 返回最小键 / 最小键值对 | O(log n) |
lastKey() / lastEntry() | 返回最大键 / 最大键值对 | O(log n) |
lowerKey(K key) | 返回严格小于 key 的最大键 | O(log n) |
floorKey(K key) | 返回小于等于 key 的最大键 | O(log n) |
ceilingKey(K key) | 返回大于等于 key 的最小键 | O(log n) |
higherKey(K key) | 返回严格大于 key 的最小键 | O(log n) |
pollFirstEntry() | 移除并返回最小键值对 | O(log n) |
pollLastEntry() | 移除并返回最大键值对 | O(log n) |
范围视图方法
| 方法 | 描述 |
|---|---|
subMap(K fromKey, K toKey) | 返回 [fromKey, toKey) 子 Map(默认左闭右开) |
subMap(K from, boolean fromInclusive, K to, boolean toInclusive) | 可指定边界是否包含 |
headMap(K toKey) | 返回键 < toKey 的视图 |
headMap(K toKey, boolean inclusive) | 可指定是否包含 toKey |
tailMap(K fromKey) | 返回键 >= fromKey 的视图 |
tailMap(K fromKey, boolean inclusive) | 可指定是否包含 fromKey |
descendingMap() | 返回逆序视图 |
navigableKeySet() | 返回键的 NavigableSet 视图 |
descendingKeySet() | 返回键的逆序 NavigableSet 视图 |
核心原理:红黑树
红黑树的五条性质
红黑树的核心价值:通过颜色约束 + 旋转操作维持树的近似平衡,保证插入、删除、查找的最坏时间复杂度均为 O(log n)。与 AVL 树(严格平衡,旋转次数更多)相比,红黑树在插入删除时重新着色的开销更小,实际性能更优。
红黑树插入与重平衡流程
旋转操作状态图
左旋示意图
左旋(以 P 为轴):
P Q
/ \ / \
A Q => P C
/ \ / \
B C A B
操作步骤:
1. Q = P.right
2. P.right = Q.left (B)
3. 如果 B != null, B.parent = P
4. Q.parent = P.parent (替换 P 在父节点中的位置)
5. Q.left = P
6. P.parent = Q
两种排序方式
方式一:自然排序(Comparable)
键对象实现 Comparable<T> 接口,重写 compareTo(T o) 方法:
public interface Comparable<T> {
int compareTo(T o);
// 返回负整数:this < o
// 返回 0: this == o
// 返回正整数:this > o
}
TreeMap 构造时不传 Comparator,自动使用键的自然顺序。
方式二:定制排序(Comparator)
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
// 返回负整数:o1 < o2
// 返回 0: o1 == o2
// 返回正整数:o1 > o2
}
通过 TreeMap 构造器传入 Comparator。Java 8 中可用 Lambda 表达式:
TreeMap<Employee, String> bySalary =
new TreeMap<>((a, b) -> Double.compare(a.getSalary(), b.getSalary()));
排序方式对比
| 维度 | Comparable(自然排序) | Comparator(定制排序) |
|---|---|---|
| 包 | java.lang | java.util |
| 核心方法 | compareTo(T o) | compare(T o1, T o2) |
| 实现位置 | 元素类自身实现(侵入) | 外部独立实现(无侵入) |
| 耦合度 | 高——排序逻辑侵入类代码 | 低——策略模式,解耦 |
| 排序数量 | 仅一种(唯一的自然顺序) | 可定义任意多种 |
| 使用场景 | 元素有"天然"唯一排序 | 需要多种排序、元素不可修改 |
| TreeMap 构造 | new TreeMap<>() | new TreeMap<>(comparator) |
完整代码示例
示例一:飞翔科技员工按薪资排序存储
import java.util.*;
/**
* 场景:大翔要求在年终调薪报表中,按薪资从低到高查看所有员工信息。
* 白歌选择了 TreeMap,以便同时获取最低薪资和最高薪资员工。
* 小崔负责实现排序逻辑,孔蓝验证了各种边界情况。
*/
public class FeiXiangSalaryTreeMap {
// 键:薪资(Double),值:员工姓名
// 使用自然排序(Double 实现了 Comparable)
public static void main(String[] args) {
// ========== 1. 创建 TreeMap(自然排序:按薪资升序) ==========
TreeMap<Double, List<String>> salaryMap = new TreeMap<>();
// 因为相同薪资的员工可能存在多个,用 List 存储同名员工
addEmployee(salaryMap, 80000.0, "大翔");
addEmployee(salaryMap, 35000.0, "白歌");
addEmployee(salaryMap, 12000.0, "小崔");
addEmployee(salaryMap, 15000.0, "孔蓝");
addEmployee(salaryMap, 22000.0, "Frank");
addEmployee(salaryMap, 18000.0, "黄俪");
addEmployee(salaryMap, 35000.0, "赵鸣"); // 与白歌相同薪资
addEmployee(salaryMap, 12000.0, "孙鹤"); // 与小崔相同薪资
// ========== 2. 展示全部排序结果 ==========
System.out.println("=== 飞翔科技薪资排名(从低到高) ===");
int rank = 1;
for (Map.Entry<Double, List<String>> entry : salaryMap.entrySet()) {
for (String name : entry.getValue()) {
System.out.printf(" 第%d名: %s → ¥%.0f%n", rank++, name, entry.getKey());
}
}
// ========== 3. 导航方法 ==========
System.out.println("\n=== 导航查询 ===");
System.out.println("最低薪资员工: " + salaryMap.firstEntry().getValue()
+ " → ¥" + salaryMap.firstKey());
System.out.println("最高薪资员工: " + salaryMap.lastEntry().getValue()
+ " → ¥" + salaryMap.lastKey());
// lowerKey:严格小于
System.out.println("\n薪资严格低于 ¥20000 的最高薪资: ¥" + salaryMap.lowerKey(20000.0));
// floorKey:小于等于
System.out.println("薪资 ≤ ¥20000 的最高薪资: ¥" + salaryMap.floorKey(20000.0));
// ceilingKey:大于等于
System.out.println("薪资 ≥ ¥20000 的最低薪资: ¥" + salaryMap.ceilingKey(20000.0));
// higherKey:严格大于
System.out.println("薪资严格高于 ¥20000 的最低薪资: ¥" + salaryMap.higherKey(20000.0));
// ========== 4. 范围查询 ==========
System.out.println("\n=== 范围查询 ===");
// subMap: [15000, 35000) —— 左闭右开
SortedMap<Double, List<String>> midRange = salaryMap.subMap(15000.0, 35000.0);
System.out.println("薪资在 [¥15000, ¥35000) 范围内:");
for (Map.Entry<Double, List<String>> entry : midRange.entrySet()) {
System.out.println(" ¥" + entry.getKey() + " → " + entry.getValue());
}
// headMap: < 20000
System.out.println("\n薪资 < ¥20000 的员工:");
salaryMap.headMap(20000.0).forEach((salary, names) ->
System.out.println(" ¥" + salary + " → " + names)
);
// tailMap: >= 30000
System.out.println("\n薪资 >= ¥30000 的员工:");
salaryMap.tailMap(30000.0).forEach((salary, names) ->
System.out.println(" ¥" + salary + " → " + names)
);
// ========== 5. 逆序视图 ==========
System.out.println("\n=== 逆序视图(从高到低) ===");
NavigableMap<Double, List<String>> descending = salaryMap.descendingMap();
for (Map.Entry<Double, List<String>> entry : descending.entrySet()) {
for (String name : entry.getValue()) {
System.out.println(" " + name + " → ¥" + entry.getKey());
}
}
// ========== 6. pollFirstEntry / pollLastEntry:取出并移除 ==========
TreeMap<Double, String> copyMap = new TreeMap<>();
copyMap.put(80000.0, "大翔");
copyMap.put(12000.0, "小崔");
copyMap.put(35000.0, "白歌");
System.out.println("\n=== 取出最值(poll) ===");
Map.Entry<Double, String> lowest = copyMap.pollFirstEntry();
System.out.println("取出最低: " + lowest.getKey() + " → " + lowest.getValue());
System.out.println("剩余: " + copyMap);
Map.Entry<Double, String> highest = copyMap.pollLastEntry();
System.out.println("取出最高: " + highest.getKey() + " → " + highest.getValue());
System.out.println("剩余: " + copyMap);
}
/** 向 TreeMap 添加员工,处理同薪资多人情况 */
private static void addEmployee(TreeMap<Double, List<String>> map, double salary, String name) {
map.computeIfAbsent(salary, k -> new ArrayList<>()).add(name);
}
}
=== 飞翔科技薪资排名(从低到高) ===
第1名: 小崔 → ¥12000
第2名: 孙鹤 → ¥12000
第3名: 孔蓝 → ¥15000
第4名: 黄俪 → ¥18000
第5名: Frank → ¥22000
第6名: 白歌 → ¥35000
第7名: 赵鸣 → ¥35000
第8名: 大翔 → ¥80000
=== 导航查询 ===
最低薪资员工: [小崔, 孙鹤] → ¥12000
最高薪资员工: [大翔] → ¥80000
薪资严格低于 ¥20000 的最高薪资: ¥18000
薪资 ≤ ¥20000 的最高薪资: ¥18000
薪资 ≥ ¥20000 的最低薪资: ¥22000
薪资严格高于 ¥20000 的最低薪资: ¥22000
=== 范围查询 ===
薪资在 [¥15000, ¥35000) 范围内:
¥15000 → [孔蓝]
¥18000 → [黄俪]
¥22000 → [Frank]
薪资 < ¥20000 的员工:
¥12000 → [小崔, 孙鹤]
¥15000 → [孔蓝]
¥18000 → [黄俪]
薪资 >= ¥30000 的员工:
¥35000 → [白歌, 赵鸣]
¥80000 → [大翔]
=== 逆序视图(从高到低) ===
大翔 → ¥80000
白歌 → ¥35000
赵鸣 → ¥35000
Frank → ¥22000
黄俪 → ¥18000
孔蓝 → ¥15000
小崔 → ¥12000
孙鹤 → ¥12000
=== 取出最值(poll) ===
取出最低: 12000.0 → 小崔
剩余: {35000.0=白歌, 80000.0=大翔}
取出最高: 80000.0 → 大翔
剩余: {35000.0=白歌}
示例二:按日期范围查询订单 —— Frank 的业务场景
import java.util.*;
import java.text.SimpleDateFormat;
/**
* 场景:Frank 负责的市场部需要查询指定日期范围内的客户订单。
* 白歌建议使用 TreeMap 以日期为键,利用 subMap 快速定位范围。
* 小崔实现了自定义 Comparator,支持逆序和范围查询。
*/
public class FeiXiangOrderDateQuery {
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
/** 订单实体 */
static class Order {
final String orderId;
final String customer;
final double amount;
Order(String orderId, String customer, double amount) {
this.orderId = orderId;
this.customer = customer;
this.amount = amount;
}
@Override
public String toString() {
return String.format("%s | %s | ¥%.0f", orderId, customer, amount);
}
}
public static void main(String[] args) throws Exception {
// ========== 1. 按日期自然排序的 TreeMap ==========
TreeMap<Date, List<Order>> orderMap = new TreeMap<>();
addOrder(orderMap, "2024-01-05", new Order("ORD001", "大客户A", 50000));
addOrder(orderMap, "2024-01-15", new Order("ORD002", "大客户B", 35000));
addOrder(orderMap, "2024-02-01", new Order("ORD003", "大客户C", 120000));
addOrder(orderMap, "2024-02-10", new Order("ORD004", "大客户A", 80000));
addOrder(orderMap, "2024-03-05", new Order("ORD005", "大客户B", 65000));
addOrder(orderMap, "2024-03-20", new Order("ORD006", "大客户C", 42000));
// ========== 2. 展示全部订单(按日期排序) ==========
System.out.println("=== 飞翔科技 2024 Q1 订单(按日期排序) ===");
for (Map.Entry<Date, List<Order>> entry : orderMap.entrySet()) {
System.out.println("\n " + sdf.format(entry.getKey()) + ":");
for (Order o : entry.getValue()) {
System.out.println(" " + o);
}
}
// ========== 3. 范围查询:查询 1 月份的订单 ==========
Date jan1 = sdf.parse("2024-01-01");
Date feb1 = sdf.parse("2024-02-01");
System.out.println("\n=== 范围查询:2024年1月订单 [1/1, 2/1) ===");
SortedMap<Date, List<Order>> janOrders = orderMap.subMap(jan1, feb1);
for (Map.Entry<Date, List<Order>> entry : janOrders.entrySet()) {
for (Order o : entry.getValue()) {
System.out.println(" " + sdf.format(entry.getKey()) + " → " + o);
}
}
// ========== 4. 范围查询:查询 2 月及以后的订单 ==========
System.out.println("\n=== 范围查询:2024年2月及以后 [2/1, ...) ===");
SortedMap<Date, List<Order>> febOnwards = orderMap.tailMap(feb1);
for (Map.Entry<Date, List<Order>> entry : febOnwards.entrySet()) {
for (Order o : entry.getValue()) {
System.out.println(" " + sdf.format(entry.getKey()) + " → " + o);
}
}
// ========== 5. 查询特定日期前后最近的订单 ==========
Date target = sdf.parse("2024-02-05");
System.out.println("\n=== 导航查询:以 " + sdf.format(target) + " 为基准 ===");
System.out.println("该日期之前最近的订单日期: " + sdf.format(orderMap.lowerKey(target)));
System.out.println("该日期之后最近的订单日期: " + sdf.format(orderMap.higherKey(target)));
// ========== 6. 逆序:最近订单在前 ==========
System.out.println("\n=== 逆序视图(最新订单在前) ===");
NavigableMap<Date, List<Order>> reversed = orderMap.descendingMap();
for (Map.Entry<Date, List<Order>> entry : reversed.entrySet()) {
for (Order o : entry.getValue()) {
System.out.println(" " + sdf.format(entry.getKey()) + " → " + o);
}
}
// ========== 7. 使用自定义 Comparator 按金额排序 ==========
System.out.println("\n=== 自定义排序:按订单金额从高到低 ===");
TreeMap<Double, Order> byAmount = new TreeMap<>(
Comparator.reverseOrder() // 降序
);
for (List<Order> orders : orderMap.values()) {
for (Order o : orders) {
byAmount.put(o.amount, o);
}
}
byAmount.forEach((amount, order) ->
System.out.println(" ¥" + amount + " → " + order)
);
}
private static void addOrder(TreeMap<Date, List<Order>> map, String dateStr, Order order)
throws Exception {
Date date = sdf.parse(dateStr);
map.computeIfAbsent(date, k -> new ArrayList<>()).add(order);
}
}
=== 飞翔科技 2024 Q1 订单(按日期排序) ===
2024-01-05:
ORD001 | 大客户A | ¥50000
2024-01-15:
ORD002 | 大客户B | ¥35000
2024-02-01:
ORD003 | 大客户C | ¥120000
2024-02-10:
ORD004 | 大客户A | ¥80000
2024-03-05:
ORD005 | 大客户B | ¥65000
2024-03-20:
ORD006 | 大客户C | ¥42000
=== 范围查询:2024年1月订单 [1/1, 2/1) ===
2024-01-05 → ORD001 | 大客户A | ¥50000
2024-01-15 → ORD002 | 大客户B | ¥35000
=== 范围查询:2024年2月及以后 [2/1, ...) ===
2024-02-01 → ORD003 | 大客户C | ¥120000
2024-02-10 → ORD004 | 大客户A | ¥80000
2024-03-05 → ORD005 | 大客户B | ¥65000
2024-03-20 → ORD006 | 大客户C | ¥42000
=== 导航查询:以 2024-02-05 为基准 ===
该日期之前最近的订单日期: 2024-02-01
该日期之后最近的订单日期: 2024-02-10
=== 逆序视图(最新订单在前) ===
2024-03-20 → ORD006 | 大客户C | ¥42000
2024-03-05 → ORD005 | 大客户B | ¥65000
2024-02-10 → ORD004 | 大客户A | ¥80000
2024-02-01 → ORD003 | 大客户C | ¥120000
2024-01-15 → ORD002 | 大客户B | ¥35000
2024-01-05 → ORD001 | 大客户A | ¥50000
=== 自定义排序:按订单金额从高到低 ===
¥120000 → ORD003 | 大客户C | ¥120000
¥80000 → ORD004 | 大客户A | ¥80000
¥65000 → ORD005 | 大客户B | ¥65000
¥50000 → ORD001 | 大客户A | ¥50000
¥42000 → ORD006 | 大客户C | ¥42000
¥35000 → ORD002 | 大客户B | ¥35000
易错场景
反例一:Comparator 返回 0 导致键覆盖
小崔在写按部门排序的 TreeMap 时,仅以部门名作为比较规则:
// ❌ 错误:Comparator 仅按部门比较,同部门的员工会互相覆盖!
TreeMap<String, String> deptMap = new TreeMap<>(
(a, b) -> {
// a = "EMP001-技术部-白歌" 格式的字符串
String deptA = a.split("-")[1];
String deptB = b.split("-")[1];
return deptA.compareTo(deptB); // 同部门返回 0 → 视为相同键!
}
);
deptMap.put("EMP001-技术部-白歌", "白歌信息");
deptMap.put("EMP002-技术部-小崔", "小崔信息");
System.out.println(deptMap.size()); // 1!小崔覆盖了白歌!
原理:TreeMap 判断键是否相等完全依赖 compareTo/compare 的返回值。返回 0 即视为相同键,新值覆盖旧值(与 HashMap 用 equals 完全不同)。
纠正:
// ✅ 正确:Comparator 必须处理所有可能情况,不能轻易返回 0
TreeMap<String, String> deptMap = new TreeMap<>(
(a, b) -> {
String deptA = a.split("-")[1];
String deptB = b.split("-")[1];
int cmp = deptA.compareTo(deptB);
if (cmp != 0) return cmp;
// 同部门时,用完整字符串区分(保证不同键肯定不返回 0)
return a.compareTo(b);
}
);
deptMap.put("EMP001-技术部-白歌", "白歌信息");
deptMap.put("EMP002-技术部-小崔", "小崔信息");
System.out.println(deptMap.size()); // 2 —— 正确!
反例二:TreeMap 中使用 null 键导致 NPE
// ❌ 错误:TreeMap 不允许 null 键
TreeMap<String, String> map = new TreeMap<>();
map.put("A", "1");
map.put(null, "2"); // NullPointerException!
// TreeMap.put 内部调用 compareTo/compare,null.compareTo() 或 comparator.compare(null, obj) 抛 NPE
纠正:
// ✅ 如果确实需要表示"空键",使用特殊标记值
TreeMap<String, String> map = new TreeMap<>();
map.put("A", "1");
map.put("__NULL_KEY__", "2"); // 用特殊字符串代替 null
// 或者在插入前做 null 检查
反例三:subMap 返回的是视图,修改受边界限制
// ❌ 错误:向 subMap 视图中插入超出边界范围的元素
TreeMap<Integer, String> map = new TreeMap<>();
map.put(1, "A"); map.put(3, "C"); map.put(5, "E");
SortedMap<Integer, String> sub = map.subMap(1, 5); // [1, 5)
System.out.println(sub); // {1=A, 3=C}
sub.put(6, "F"); // IllegalArgumentException: key out of range!
// subMap 有边界检查,超出 [1, 5) 范围的插入会拒绝
纠正:
// ✅ 向 subMap 插入时确保在边界范围内
sub.put(2, "B"); // 在范围内,OK
sub.put(4, "D"); // 在范围内,OK
System.out.println(sub); // {1=A, 2=B, 3=C, 4=D}
System.out.println(map); // {1=A, 2=B, 3=C, 4=D, 5=E} —— 原 map 也被修改了!
反例四:未正确实现 Comparable 导致元素丢失
// ❌ 错误:compareTo 只用部分字段,信息丢失
class Employee implements Comparable<Employee> {
String name;
double salary;
@Override
public int compareTo(Employee o) {
return Double.compare(this.salary, o.salary); // 只用薪资比较
}
}
TreeMap<Employee, String> map = new TreeMap<>();
map.put(new Employee("小崔", 12000), "dept");
map.put(new Employee("孙鹤", 12000), "dept"); // 被认为与"小崔"是同一键!
System.out.println(map.size()); // 1 —— 孙鹤丢失!
纠正:确保 compareTo 覆盖所有决定唯一性的字段,或使用 Comparator.comparing().thenComparing()。
面试考点
Q1:TreeMap 与 HashMap 的区别?各自适用场景?
维度 TreeMap HashMap 底层结构 红黑树 数组 + 链表 + 红黑树 键的顺序 有序(自然顺序或 Comparator) 无序(不保证) get/put 时间 O(log n) 平均 O(1),最坏 O(log n) null 键 不允许 允许一个 导航方法 丰富(lowerKey、subMap 等) 无 内存占用 较大(每节点 4 个引用) 较小 适用场景 需要键排序、范围查询、最值快速获取 快速查找、不关心顺序 如果业务需要按键排序(如排行榜、日期排序),用 TreeMap;如果只需要快速查找,用 HashMap。
Q2:红黑树相比普通二叉搜索树的优势是什么?
普通二叉搜索树(BST)在最坏情况下(如按顺序插入递增/递减的数据)会退化成链表,时间复杂度从 O(log n) 退化为 O(n)。红黑树通过五大性质(特别是"没有连续红色节点"和"所有路径黑色节点数相同")保证树高始终保持在 O(log n) 级别。具体来说,红黑树保证最长路径不超过最短路径的 2 倍,从而确保查找、插入、删除的最坏时间复杂度为 O(log n)。与 AVL 树(严格平衡,左子树和右子树高度差不超过 1)相比,红黑树牺牲了部分平衡性(允许 2 倍差距),换取了更少的旋转次数,在插入频繁的场景下实际性能更优。
Q3:TreeMap 的 subMap/headMap/tailMap 返回的是副本还是视图?有哪些注意事项?
返回的是视图(view),不是独立副本。这意味着: ① 双向影响:对视图的修改会反映到原 TreeMap,反之亦然; ② 边界限制:向 subMap 插入超出边界范围的元素会抛
IllegalArgumentException; ③ 视图失效:如果原 TreeMap 的结构(非视图本身的操作)被修改,视图会抛ConcurrentModificationException; ④ 需要副本时,应使用new TreeMap<>(map.subMap(from, to))创建独立拷贝。这也是为什么
subMap不是线程安全的——视图和被视图的 Map 共享同一棵红黑树。
Q4:TreeMap 的 Comparator 返回 0 意味着什么?与 HashMap 的 equals 有何不同?
TreeMap 判断两个键是否相等完全依赖
compareTo或compare方法的返回值。当返回 0 时,TreeMap 认为两个键相等,put 新值会覆盖旧值。这与 HashMap 的等价判断机制有本质区别:HashMap 先通过 hashCode 定位桶,再通过 equals 判断键是否相同。重要结论: ① TreeMap 不看
equals,只看compareTo/compare; ② 若 Comparator 返回 0 的两个对象,即使它们不是逻辑上的"同一个东西",TreeMap 也会视为重复键; ③ Set 接口的通用契约建议compareTo返回 0 时equals也返回 true(但 TreeSet/TreeMap 实现本身并不检查 equals)。因此编写 Comparator 时必须保证:只有真正需要视为"相同键"的情况才返回 0,否则应用次要字段兜底。
Q5:为什么 TreeMap 允许 null 值但不允许 null 键?
值(value)不参与键的比较和排序,可以作为普通对象存储和检索,null 值在语义上合理(如"数据尚未填写")。但键(key)必须参与排序——插入、查找、删除时都要调用
compareTo或compare方法进行比较。null.compareTo(obj)或comparator.compare(null, obj)会直接抛出NullPointerException。从设计哲学角度,null 没有"顺序"概念,放入有序集合中语义不清。HashMap 允许 null 键是因为它只需计算hashCode(JVM 为 null 特殊处理返回 0),不需要比较。