1. 概述

本文将深入探讨 Java 集合框架(JCF)中 Map 接口的 TreeMap 实现类。

TreeMap 是一种特殊的 Map 实现,它根据键的自然顺序或构造时提供的 Comparator 对条目进行排序。之前我们研究过 HashMapLinkedHashMap 的实现,会发现这些类的工作原理有很多相似之处。

建议在阅读本文前先了解上述两篇文章,这样能更好地理解 TreeMap 的特性。

2. TreeMap 的默认排序

默认情况下,TreeMap 会按照键的自然顺序对所有条目进行排序。对于整数来说就是升序,对于字符串则是字母顺序。

下面通过测试验证自然排序:

@Test
public void givenTreeMap_whenOrdersEntriesNaturally_thenCorrect() {
    TreeMap<Integer, String> map = new TreeMap<>();
    map.put(3, "val");
    map.put(2, "val");
    map.put(1, "val");
    map.put(5, "val");
    map.put(4, "val");

    assertEquals("[1, 2, 3, 4, 5]", map.keySet().toString());
}

注意我们以无序方式插入整数键,但检索键集时,它们确实按升序排列。这就是整数的自然顺序。

同样地,使用字符串时也会按自然顺序(字母顺序)排序:

@Test
public void givenTreeMap_whenOrdersEntriesNaturally_thenCorrect2() {
    TreeMap<String, String> map = new TreeMap<>();
    map.put("c", "val");
    map.put("b", "val");
    map.put("a", "val");
    map.put("e", "val");
    map.put("d", "val");

    assertEquals("[a, b, c, d, e]", map.keySet().toString());
}

HashMapLinkedHashMap 不同,TreeMap 完全不使用哈希原理,因为它不依赖数组来存储条目。

3. TreeMap 的自定义排序

如果对 TreeMap 的自然排序不满意,我们可以在构造时通过 Comparator 定义自己的排序规则。

下面的示例中,我们希望整数键按降序排列:

@Test
public void givenTreeMap_whenOrdersEntriesByComparator_thenCorrect() {
    TreeMap<Integer, String> map = 
      new TreeMap<>(Comparator.reverseOrder());
    map.put(3, "val");
    map.put(2, "val");
    map.put(1, "val");
    map.put(5, "val");
    map.put(4, "val");
        
    assertEquals("[5, 4, 3, 2, 1]", map.keySet().toString());
}

HashMap 不保证键的存储顺序,且不保证该顺序随时间保持不变,但 TreeMap 保证键始终按指定顺序排序。

4. TreeMap 排序的重要性

既然 TreeMap 以有序方式存储所有条目,我们就可以执行一些特殊查询:

  • 查找"最大"键
  • 查找"最小"键
  • 查找小于/大于某个值的所有键

以下代码展示了部分查询场景:

@Test
public void givenTreeMap_whenPerformsQueries_thenCorrect() {
    TreeMap<Integer, String> map = new TreeMap<>();
    map.put(3, "val");
    map.put(2, "val");
    map.put(1, "val");
    map.put(5, "val");
    map.put(4, "val");
        
    Integer highestKey = map.lastKey();
    Integer lowestKey = map.firstKey();
    Set<Integer> keysLessThan3 = map.headMap(3).keySet();
    Set<Integer> keysGreaterThanEqTo3 = map.tailMap(3).keySet();

    assertEquals(new Integer(5), highestKey);
    assertEquals(new Integer(1), lowestKey);
    assertEquals("[1, 2]", keysLessThan3.toString());
    assertEquals("[3, 4, 5]", keysGreaterThanEqTo3.toString());
}

5. TreeMap 的内部实现

TreeMap 实现了 NavigableMap 接口,其内部工作原理基于红黑树

public class TreeMap<K,V> extends AbstractMap<K,V>
  implements NavigableMap<K,V>, Cloneable, java.io.Serializable

红黑树的原理超出了本文范围,但需要记住几个关键点:

首先,红黑树是由节点组成的数据结构。可以想象一棵倒置的芒果树,根在天空,树枝向下生长。根节点包含添加到树的第一个元素。

规则是从根节点开始,任何节点的左分支元素都小于该节点本身,右分支元素则大于。大小比较由元素的自然顺序或构造时定义的 Comparator 决定。

这个规则保证了 TreeMap 的条目始终处于有序且可预测的状态。

其次,红黑树是自平衡二叉搜索树。这个特性加上上述规则,确保了基本操作(搜索、获取、插入、删除)的时间复杂度为对数级 *O(log n)*。

自平衡是关键。随着条目的不断插入和删除,树可能会在某一边变长,另一边变短。这意味着在较短的分支上操作更快,而在离根最远的分支上操作更慢——这是我们不愿看到的。

红黑树的设计解决了这个问题。每次插入和删除后,树的最大高度都维持在 *O(log n)*,即树会持续自我平衡。

HashMapLinkedHashMap 一样,TreeMap 也是非线程安全的。在多线程环境中的使用规则与前两者相同。

6. 如何选择合适的 Map 实现

研究过 HashMapLinkedHashMap 和现在的 TreeMap 后,有必要简要比较三者,帮助我们在不同场景下做出选择。

HashMap: ✅ 通用型 Map 实现,提供快速的存储和检索操作
❌ 条目排列混乱无序
⚠️ 大量迭代场景性能较差(遍历受底层数组容量影响)

LinkedHashMap: ✅ 继承 HashMap 的优点,同时保持条目插入顺序
✅ 迭代性能优异(只与条目数有关,与容量无关)
❌ 排序能力有限

TreeMap: ✅ 提供完全可控的键排序
✅ 支持范围查询等高级操作
❌ 通用性能不如前两者
⚠️ 自定义排序需实现 Comparator,可能踩坑

简单粗暴地说:**LinkedHashMap 在不牺牲 HashMap 性能的前提下解决了其无序问题,又避免了 TreeMap 的性能开销**。

7. 总结

本文深入探讨了 Java TreeMap 类及其内部实现。作为常见 Map 接口实现的压轴之作,我们还简要讨论了它与其他两种实现的适用场景对比。

本文所有示例的完整源代码可在 GitHub 项目 中找到。


原始标题:A Guide to TreeMap in Java | Baeldung