1. 概述
本文将深入探讨 Java 集合框架中的核心组件之一——TreeSet,这是最常用的 Set 实现之一。我们将从基础特性到高级用法,全面解析这个有序集合。
2. TreeSet 基础
简单来说,TreeSet 是一个有序集合,它继承自 AbstractSet 类并实现了 NavigableSet 接口。以下是它的核心特性:
- ✅ 存储唯一元素
- ❌ 不保留插入顺序
- ✅ 元素按升序排序
- ❌ 非线程安全
TreeSet 内部使用自平衡二叉搜索树(红黑树)实现。每个节点包含一个额外的颜色位(红/黑),在插入和删除时通过颜色调整保持树的大致平衡。
创建 TreeSet 实例:
Set<String> treeSet = new TreeSet<>();
2.1. 带比较器的 TreeSet
我们可以通过构造函数指定排序规则,使用 Comparable 或 Comparator:
Set<String> treeSet = new TreeSet<>(Comparator.comparing(String::length));
虽然 TreeSet 非线程安全,但可通过 Collections.synchronizedSet() 包装实现同步:
Set<String> syncTreeSet = Collections.synchronizedSet(treeSet);
3. TreeSet add() 方法
add() 方法用于添加元素,成功返回 true,元素已存在则返回 false。方法契约规定:仅当元素不存在时才添加。
@Test
public void whenAddingElement_shouldAddElement() {
Set<String> treeSet = new TreeSet<>();
assertTrue(treeSet.add("String Added"));
}
add() 方法揭示了 TreeSet 的内部实现:它依赖 TreeMap 的 put() 方法存储元素:
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
其中 m 是内部 NavigableMap(TreeMap 实现):
private transient NavigableMap<E, Object> m;
4. TreeSet contains() 方法
contains() 检查元素是否存在,存在返回 true,否则 false:
@Test
public void whenCheckingForElement_shouldSearchForElement() {
Set<String> treeSetContains = new TreeSet<>();
treeSetContains.add("String Added");
assertTrue(treeSetContains.contains("String Added"));
}
5. TreeSet remove() 方法
remove() 删除指定元素,成功删除返回 true:
@Test
public void whenRemovingElement_shouldRemoveElement() {
Set<String> removeFromTreeSet = new TreeSet<>();
removeFromTreeSet.add("String Added");
assertTrue(removeFromTreeSet.remove("String Added"));
}
6. TreeSet clear() 方法
clear() 清空所有元素:
@Test
public void whenClearingTreeSet_shouldClearTreeSet() {
Set<String> clearTreeSet = new TreeSet<>();
clearTreeSet.add("String Added");
clearTreeSet.clear();
assertTrue(clearTreeSet.isEmpty());
}
7. TreeSet size() 方法
size() 返回元素数量:
@Test
public void whenCheckingTheSizeOfTreeSet_shouldReturnThesize() {
Set<String> treeSetSize = new TreeSet<>();
treeSetSize.add("String Added");
assertEquals(1, treeSetSize.size());
}
8. TreeSet isEmpty() 方法
isEmpty() 判断集合是否为空:
@Test
public void whenCheckingForEmptyTreeSet_shouldCheckForEmpty() {
Set<String> emptyTreeSet = new TreeSet<>();
assertTrue(emptyTreeSet.isEmpty());
}
9. TreeSet iterator() 方法
iterator() 返回升序迭代器,迭代器是 fail-fast 的:
@Test
public void whenIteratingTreeSet_shouldIterateTreeSetInAscendingOrder() {
Set<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Second");
treeSet.add("Third");
Iterator<String> itr = treeSet.iterator();
while (itr.hasNext()) {
System.out.println(itr.next());
}
}
TreeSet 还支持降序迭代:
@Test
public void whenIteratingTreeSet_shouldIterateTreeSetInDescendingOrder() {
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Second");
treeSet.add("Third");
Iterator<String> itr = treeSet.descendingIterator();
while (itr.hasNext()) {
System.out.println(itr.next());
}
}
迭代期间若非通过迭代器修改集合,会抛 ConcurrentModificationException:
@Test(expected = ConcurrentModificationException.class)
public void whenModifyingTreeSetWhileIterating_shouldThrowException() {
Set<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Second");
treeSet.add("Third");
Iterator<String> itr = treeSet.iterator();
while (itr.hasNext()) {
itr.next();
treeSet.remove("Second"); // ❌ 直接修改集合
}
}
正确做法是使用迭代器的 remove():
@Test
public void whenRemovingElementUsingIterator_shouldRemoveElement() {
Set<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Second");
treeSet.add("Third");
Iterator<String> itr = treeSet.iterator();
while (itr.hasNext()) {
String element = itr.next();
if (element.equals("Second"))
itr.remove(); // ✅ 安全删除
}
assertEquals(2, treeSet.size());
}
⚠️ fail-fast 行为不保证:在未同步的并发修改中无法提供硬性保证。
10. TreeSet first() 方法
first() 返回首个元素,空集合抛 NoSuchElementException:
@Test
public void whenCheckingFirstElement_shouldReturnFirstElement() {
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("First");
assertEquals("First", treeSet.first());
}
11. TreeSet last() 方法
last() 返回末尾元素:
@Test
public void whenCheckingLastElement_shouldReturnLastElement() {
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Last");
assertEquals("Last", treeSet.last());
}
12. TreeSet subSet() 方法
subSet() 返回 [fromElement, toElement) 范围内的元素:
@Test
public void whenUsingSubSet_shouldReturnSubSetElements() {
SortedSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
treeSet.add(2);
treeSet.add(3);
treeSet.add(4);
treeSet.add(5);
treeSet.add(6);
Set<Integer> expectedSet = new TreeSet<>();
expectedSet.add(2);
expectedSet.add(3);
expectedSet.add(4);
expectedSet.add(5);
Set<Integer> subSet = treeSet.subSet(2, 6);
assertEquals(expectedSet, subSet);
}
13. TreeSet headSet() 方法
headSet() 返回小于指定元素的所有元素:
@Test
public void whenUsingHeadSet_shouldReturnHeadSetElements() {
SortedSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
treeSet.add(2);
treeSet.add(3);
treeSet.add(4);
treeSet.add(5);
treeSet.add(6);
Set<Integer> subSet = treeSet.headSet(6);
assertEquals(subSet, treeSet.subSet(1, 6));
}
14. TreeSet tailSet() 方法
tailSet() 返回大于等于指定元素的所有元素:
@Test
public void whenUsingTailSet_shouldReturnTailSetElements() {
NavigableSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
treeSet.add(2);
treeSet.add(3);
treeSet.add(4);
treeSet.add(5);
treeSet.add(6);
Set<Integer> subSet = treeSet.tailSet(3);
assertEquals(subSet, treeSet.subSet(3, true, 6, true));
}
15. 存储 Null 元素
Java 7 之前可向空 TreeSet 添加 null,但这被视为 bug。TreeSet 现已不再支持 null。
添加元素时需排序,null 无法与任何值比较,导致 NullPointerException:
@Test(expected = NullPointerException.class)
public void whenAddingNullToNonEmptyTreeSet_shouldThrowException() {
Set<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add(null); // ❌ 抛出异常
}
元素必须实现 Comparable 或被指定 Comparator 接受,且元素间必须可相互比较:
class Element {
private Integer id;
// 其他方法...
}
Comparator<Element> comparator = (ele1, ele2) -> {
return ele1.getId().compareTo(ele2.getId());
};
@Test
public void whenUsingComparator_shouldSortAndInsertElements() {
Set<Element> treeSet = new TreeSet<>(comparator);
Element ele1 = new Element();
ele1.setId(100);
Element ele2 = new Element();
ele2.setId(200);
treeSet.add(ele1);
treeSet.add(ele2);
System.out.println(treeSet);
}
16. TreeSet 性能分析
与 HashSet 相比,TreeSet 性能较低:
- add/remove/search 操作:O(log n)
- 遍历 n 个元素:O(n)
TreeSet 的适用场景:
- ✅ 需要保持元素有序时
- ✅ 需要频繁访问相邻元素时(利用局部性原理)
- ✅ 内存受限时(红黑树结构更紧凑)
- ✅ 数据从硬盘读取时(局部性减少 I/O 开销)
局部性原理:相似数据被频繁访问时,TreeSet 将它们存储在内存相近位置,提升访问效率。
17. 总结
本文全面解析了 Java TreeSet 的核心特性和使用方法。TreeSet 通过红黑树实现自动排序和去重,在需要有序集合的场景下是理想选择。虽然性能略低于 HashSet,但其有序性和局部性优势在特定场景下不可替代。
所有代码示例可在 GitHub 获取。