乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

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.langjava.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 的区别?各自适用场景?

维度TreeMapHashMap
底层结构红黑树数组 + 链表 + 红黑树
键的顺序有序(自然顺序或 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),不需要比较。

上一页
LinkedHashMap 详解
下一页
HashSet