TreeSet
本教程共 100 篇 · 第 76 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
本节目标:认清 TreeSet 会自动给元素排序,理解它要求元素「可比较」,学完知道它和 HashSet 该怎么选。
TreeSet 会自动排序
HashSet 不保序,LinkedHashSet 只保插入顺序。如果你想要的是「按大小/字母序自动排好」,那就用 TreeSet。
Set<Integer> set = new TreeSet<>();
set.add(30);
set.add(10);
set.add(20);
System.out.println(set); // [10, 20, 30],自动升序
字符串也行,按字典序排:
Set<String> names = new TreeSet<>();
names.add("banana");
names.add("apple");
names.add("cherry");
System.out.println(names); // [apple, banana, cherry]
它靠什么排序
TreeSet 底层是红黑树(一种自平衡的二叉查找树)。它每加一个元素,就按「谁大谁小」把它放到正确的位置,所以遍历时永远是有序的。正因为它内部始终是排好序的状态,取最大、最小、某一段都不用额外排序。
代价也很明显:每次插入、删除都要维持树的平衡,比 HashSet 的哈希定位慢一些。TreeSet 用一点性能换来了「永远有序」。
Note经验法则:要排序才用 TreeSet,纯去重用 HashSet。别为了「看起来整齐」无脑上 TreeSet,性能账要算清楚——数据量大、增删频繁时,这个差距会很明显。
元素必须「可比较」
这是 TreeSet 最容易爆错的点。它往树里放元素时,必须能比较两个元素的大小。怎么比?两条路:
第一条路:元素类自己实现 Comparable 接口,定义「自然顺序」。
record Score(int value) implements Comparable<Score> {
public int compareTo(Score o) {
return Integer.compare(this.value, o.value); // 按数值比
}
}
Set<Score> set = new TreeSet<>();
set.add(new Score(90));
set.add(new Score(75));
第二条路:构造 TreeSet 时传一个 Comparator,临时指定排序规则,不改类本身。
// 按字符串长度排序,而不是字典序
Set<String> set = new TreeSet<>((a, b) -> Integer.compare(a.length(), b.length()));
set.add("ccc");
set.add("a");
set.add("bb");
System.out.println(set); // [a, bb, ccc]
Warning把「没实现 Comparable、又没传 Comparator」的对象塞进 TreeSet,运行时会直接抛
ClassCastException。TreeSet 和 HashSet 不一样,它强制要求元素可比,否则连放都放不进去。
排序规则要自洽
Comparable 或 Comparator 写出来的顺序必须「自洽」:A 比 B 小、B 比 C 小,那 A 必须比 C 小。逻辑矛盾的话,TreeSet 会行为异常、悄悄丢元素。
record 和大多数 JDK 自带类(String、Integer 等)已经帮你写好了可靠的自然顺序,直接用即可。自己写 compareTo 时,最稳的写法是逐项用 Integer.compare 比较,而不是 a - b 相减(相减在数值很大时会溢出出错)。
有序集合的专属能力
因为 TreeSet 内部是排好序的,它能做几个 HashSet 做不到的事——取子集、取首尾:
TreeSet<Integer> set = new TreeSet<>();
for (int i = 1; i <= 10; i++) set.add(i);
System.out.println(set.first()); // 1,最小
System.out.println(set.last()); // 10,最大
System.out.println(set.subSet(3, 8)); // [3, 4, 5, 6, 7],左闭右开
System.out.println(set.headSet(5)); // [1, 2, 3, 4],小于 5
System.out.println(set.tailSet(8)); // [8, 9, 10],大于等于 8
Tip这些「范围视图」返回的是原集合的实时视图:你改了原集合,视图也会跟着变。需要快照时自己包一层
new TreeSet<>(set.subSet(...))。floor/ceiling/lower/higher这类「找最近元素」的方法也很有用,需要时用即可查文档。
三个 Set 怎么选
HashSet:只要去重、不在乎顺序,性能最好,默认首选。LinkedHashSet:去重 + 保插入顺序。TreeSet:去重 + 自动排序,元素必须可比,性能略低。
TreeSet 和 HashSet 性能怎么权衡
简单说,HashSet 的增删查平均是常量时间,几乎不随数据量增长而变慢;TreeSet 是对数时间,数据越大相对越慢,但差距在十万、百万级别才明显。所以选 TreeSet 的唯一硬理由是你「需要有序」,而不是它更快——事实上它更慢。
另一个坑:TreeSet 不允许放 null。它要靠比较来排位置,null 没法跟别的元素比大小,一放就抛 NullPointerException。而 HashSet 是允许一个 null 的。这也是两者行为不一致的地方,写代码时要注意。
如果你的需求只是「去重 + 最后排个序展示」,其实也可以先装 HashSet 去重,再 new ArrayList<>(set) 交给 Collections.sort 排序,不一定非得一直用 TreeSet 占着排序的成本。怎么权衡,看你是「边加边要序」还是「加完再排序」。
用 TreeSet 做范围统计
有序的最大好处是范围操作很自然。比如你有一堆用户的积分,想看 60 到 80 分之间有多少人,用 subSet(60, 81) 直接切一段出来数 size 就行,不用自己写循环判断。再比如想找「比某个分数高的最接近的人」,用 ceiling/floor 一步到位。这些能力 HashSet 给不了,是红黑树天然带来的红利。
不过要提醒:TreeSet 的排序依赖 Comparator 或 Comparable,一旦这两个写错(比如只用部分字段比较),去重逻辑就会跟着出错——因为 TreeSet 认为「比较结果为 0」的就是同一个元素。所以交给 TreeSet 排序的字段,一定要覆盖能唯一区分对象的全部关键信息。
一句话收尾
TreeSet 的价值在于「有序」,不是「更快」。只要你的场景需要自动排序或范围查询,它就值得用;否则老老实实用 HashSet。另外别忘了它拒绝 null、要求元素可比这两条铁律,踩过一次就记住了。排序规则写错还会顺带破坏去重,所以比较逻辑要覆盖能区分对象的全部字段,别只比其中一部分。当你拿不准该不该上 TreeSet 时,先问自己一句:我真的需要它始终保持有序吗?如果只想要去重,HashSet 永远更轻更快。
小结
TreeSet 靠红黑树自动维护有序,代价是插入略慢、元素必须可比(实现 Comparable 或传入 Comparator)。它还能做取首/尾、取子集这些有序专属操作。下一章进入队列 Queue 和双端队列 Deque。