public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
但是TreeSet实现了NavigableSet接口,这个NavigableSet接口扩展了SortedSet接口,最大的特色就是导航方法
,它是基于 红黑树
数据结构实现的,底层具体实现是基于TreeMap
底层逻辑是基于TreeMap对红黑树的管理操作方式,TreeMap详情介绍,新增和删除元素方式就不阐述了,可以参考TreeMap的机制,这里单独简单介绍下TreeSet的有序性 底层逻辑
// 直接输出TreeSet集合会输出一个排序的集合
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(80);
treeSet.add(90);
System.out.println(treeSet);//[80, 90]
//这是因为TreeSet会自动维护一个有序的集合(当然比起无序的HashSet这是额外的开销)
//调用链:iterator() → navigableKeySet().iterator()
public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}
//本质是调用TreeMap.navigableKeySet() 获取key键的合集
// 而KeyIterator 是 TreeMap 的内部类,实现了中序遍历逻辑 从而获取到正序输出Key键值合集
public Iterator<K> iterator() {
return new KeyIterator(getFirstEntry());
}
无参构造(自然排序)
//要求元素实现 Comparable 接口(如 Integer, String),适用于实现了Comparable 接口的类
TreeSet<Integer> treeSet= new TreeSet<>();
自定义比较器构造
// 降序排序
TreeSet<Integer> set2 = new TreeSet<>((a, b) -> b - a);
// 按字符串长度排序
TreeSet<String> set3 = new TreeSet<>(Comparator.comparing(String::length));
从集合初始化
List<Integer> list = Arrays.asList(5, 2, 8, 1);
TreeSet<Integer> set4 = new TreeSet<>(list); // 自动排序为 [1,2,5,8]
从有序集合复制
SortedSet<Integer> sortedSet = new TreeSet<>(Arrays.asList(9, 4, 7));
TreeSet<Integer> set5 = new TreeSet<>(sortedSet); // 继承排序规则
排序大小输出
)TreeSet 最核心的特点就是它会自动对其包含的元素进行排序
,排序方式分为自然排序
(Comparable接口)和定制排序
(Comparator)两种方式
如果创建 TreeSet 时
没有显式提供Comparator
,则要求集合中的元素必须实现 Comparable 接口
//比如使用的是默认的构造方法
public TreeSet() {
this(new TreeMap<E,Object>());
}
//而不是使用带有器的构造方法 构建的TreeSet
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
TreeSet
会调用元素的 compareTo(Object o)
方法来确定元素的顺序,例如:String (按字典序)
、Integer, Double, Date 等标准类都实现了 Comparable,所以适用于常见包装类(既已实现了Comparable的类)
TreeSet<String> words = new TreeSet<>();
words.add("dog");
words.add("apple");
words.add("cat");
// 遍历输出: apple, cat, dog (按字母顺序升序)
在创建 TreeSe
t 时,可以传入一个实现了Comparator
接口的对象
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
TreeSet 会使用这个 Comparator
的 compare(Object o1, Object o2)
方法来比较元素并确定顺序,
1.在Comparator 比较方法里面,我们可以自定义比较规则,所以叫比较排序
2.比较规则上优先级选择上 定制排序 大于 自然排序(即使元素实现了 Comparable)
// 添加元素时候会先判断定制排序是否存在
Comparator<? super K> cpr = comparator;
if (cpr != null) {
示例 (按字符串长度排序,长度相同则按字母顺序):
Comparator<String> byLengthThenNatural = Comparator
.comparingInt(String::length) // 先按长度
.thenComparing(Comparator.naturalOrder()); // 长度相同再按自然顺序
TreeSet<String> words = new TreeSet<>(byLengthThenNatural);
words.add("dog");
words.add("apple");
words.add("cat");
words.add("ant");
// 遍历输出: dog, ant, cat, apple (长度:3, 3, 3, 5; 长度相同的按字母顺序: ant, cat, dog)
和 HashSet 一样不允许存储重复的元素。尝试添加重复元素会被忽略(add 方法返回 false)
Set<String> treeSet = new TreeSet<>();
System.out.println(treeSet.add("a"));// true
System.out.println(treeSet.add("a")); //false
底层实现原理
//treeSet.add方法底层实现是调用TreeMap新增节点方法,然后根据返回值判断
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
// TreeMap数据结构 红黑树,没有重复键值的添加成功返回null,有相同的更新并返回旧的值
public V put(K key, V value) {}
TreeMap<String,String> map = new TreeMap<>();
System.out.println(map.put("1","FirstValue")); // 添加成功返回null
System.out.println(map.put("1","NextValue")); // 更新并返回旧的值FirstValue
TreeSet
内部使用红黑树数据结构来存储元素,TreeSet
底层具体实现是基于TreeMap,而TreeMap的底层树结构是红黑树
红黑树是一种自平衡的二叉查找树。这种结构保证了 TreeSet 的基本操作添加 add()、删除 remove()、查找 contains())的时间复杂度都是 O(log n)
,自平衡的二叉查找树设计核心目标就是防止二叉查找树退化成链表(复杂度 O(n))
TreeSet
实现了NavigableSet
接口 丰富的导航方法,可方便地查找与给定元素“最接近”的元素
public static void main(String[] args) {
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(80);
treeSet.add(90);
treeSet.add(70);
treeSet.add(50);
treeSet.add(60);
treeSet.add(40);
// 原始集合: [40, 50, 60, 70, 80, 90]
System.out.println("原始集合: " + treeSet);
}
首先TreeSet元素的大小输出是按照排序输出的
// 1. 基础导航方法
System.out.println("\n=== 基础导航方法 ===");
System.out.println("第一个元素: " + treeSet.first()); // 40
System.out.println("最后一个元素: " + treeSet.last()); // 90
System.out.println("大于60的最小元素: " + treeSet.higher(60)); // 70
System.out.println("小于80的最大元素: " + treeSet.lower(80)); // 70
System.out.println("大于等于70的最小元素: " + treeSet.ceiling(70)); // 70
System.out.println("小于等于70的最大元素: " + treeSet.floor(70)); // 70
System.out.println("\n=== 范围视图 ===");
// 小于范围
System.out.println("头视图(严格小于80): " + treeSet.headSet(80));
System.out.println("头视图(小于等于80): " + treeSet.headSet(80, true));
// 大于范围
System.out.println("尾视图(大于等于80): " + treeSet.tailSet(80));
System.out.println("尾视图(严格大于80): " + treeSet.tailSet(80, false));
// 子范围
System.out.println("子视图[60-90]: " + treeSet.subSet(60, 90));
System.out.println("子视图(60-90[含边界90]): " +
treeSet.subSet(60, true, 92, true));
打印输出
=== 范围视图 ===
头视图(严格小于80): [40, 50, 60, 70]
头视图(小于等于80): [40, 50, 60, 70, 80]
尾视图(大于等于80): [80, 90]
尾视图(严格大于80): [90]
子视图[60-90]: [60, 70, 80]
子视图(60-90[含边界90]): [60, 70, 80, 90]
// 3. 提取和删除元素
System.out.println("\n=== 元素操作 ===");
System.out.println("删除最小元素并返回: " + treeSet.pollFirst()); // 40
System.out.println("删除最大元素并返回: " + treeSet.pollLast()); // 90
System.out.println("当前集合: " + treeSet); // [50, 60, 70, 80]
// 4. 逆序视图
System.out.println("\n=== 逆序操作 ===");
NavigableSet<Integer> descendingScores = treeSet.descendingSet();
System.out.println("逆序集合: " + descendingScores); // [80, 70, 60, 50]
System.out.println("逆序集合的第一个元素: " + descendingScores.first()); // 80
System.out.println("逆序集合的最后一个元素: " + descendingScores.last()); // 50
TreeSet不允许null值区别于HashSet的允许一个null值
这是因为TreeSet
底层调用的是TreeMap
,底层数据是二叉搜索树
:任意节点都比左子节点大 比右节点小,新增数据需要比较查找合适的插入位置,null无法比较大小会抛出空指针的,除非在定制排序Comparator
比较器显式的处理null
比如把null作为特殊的最大或最小值存在树中
而HashSet
的底层调用的是HashMap
,底层数据是 哈希桶+(桶内数据)
, 桶内数据结构节点小于等于6是链表,大于等于8是红黑树
,如果是存在链表里面 键大小多少是不需要比较的没有影响,所以HashMap对键值为null的节点特殊处理
:
键为null的条目,被固定存储在数组索引0的桶bucket(即table[0]),始终以链表节点 Node 形式存储,永远不会转换为树节点 TreeNode
TreeSet底层实现是依靠TreeMap,而TreeMap 本身非线程安全,其内部的红黑树结构在并发修改时极易破坏平衡性
情况1:多线程同时添加元素
TreeSet<Integer> set = new TreeSet<>();
// 线程A
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
set.add(i);
}
}).start();
// 线程B
new Thread(() -> {
for (int i = 1000; i < 2000; i++) {
set.add(i);
}
}).start();
//可能结果:
// 部分元素丢失(最终 size < 2000)
// 红黑树结构破坏导致无限循环
// 抛出 ClassCastException(树节点类型损坏)
情况2:读写并发(迭代时修改)
TreeSet<String> names = new TreeSet<>(Arrays.asList("Alice", "Bob"));
// 读线程
new Thread(() -> {
for (String name : names) { // 迭代过程中
System.out.println(name);
Thread.sleep(100); // 模拟处理耗时
}
}).start();
// 写线程
new Thread(() -> {
names.add("Carol"); // 并发修改
}).start();
//可能结果:
// ConcurrentModificationException
// 迭代结果不一致(看到部分修改)
// 无限循环或程序卡死
方案一:使用Collections.synchronizedSortedSet
包装(粗粒度锁)
实现原理:通过包装器在所有方法上加 synchronized 锁
SortedSet<Integer> safeSet = Collections.synchronizedSortedSet(
new TreeSet<Integer>()
);
// 写操作(自动加锁)
safeSet.add(42);
// 读操作(需要手动同步)
synchronized (safeSet) {
for (Integer num : safeSet) {
System.out.println(num);
}
}
//适用场景:
//写操作频繁程度中等
//对吞吐量要求不高
//需要保持 TreeSet 的有序特性
方案二:使用并发集合 ConcurrentSkipListSet
(最佳实践)
ConcurrentSkipListSet:本质上是依靠 ConcurrentSkipListMap 实现的
1.线程安全:ConcurrentSkipListSet
线程安全,可在多线程环境中安全使用,而不需额外的同步措施
2.实现原理:底层使用跳表(Skip List)数据结构,这使得它在并发环境下具有较好的性能
3.有序性:根据元素的自然顺序或者通过构造函数提供的Comparator
进行排序
4.唯一性:作为一个Set,它不允许重复元素
// 创建并发安全的有序集合
ConcurrentSkipListSet<Integer> concurrentSet =
new ConcurrentSkipListSet<>();
// 多线程安全操作
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
for (int j = 0; j < 1000; j++) {
concurrentSet.add(ThreadLocalRandom.current().nextInt());
}
});
}
技术优势:
1.无锁算法:使用(内置无锁)
CAS 操作实现并发控制
2.高吞吐量:平均 O(log n) 时间复杂度
3.弱一致性迭代器:迭代时可能反映
其他线程的并发修改,不抛 ConcurrentModificationException
4.完全并发:支持读写并行
跳表结构示意图:
跳表是一种多层的有序链表,最底层(第0层)包含所有元素,每一层都是下一层的子集。上层相当于下层的“快速通道”,通过增加多层索引来提高查找效率
假设有一个包含以下整数的跳表:1, 4, 7, 9, 12, 15, 18, 20。构建一个跳表,它可能有如下结构
最高层(比如第3层):只包含极少数元素,比如头节点和9(假设随机层数分配后9有3层)。
第2层:包含头节点、7、9、20(假设这些节点被分配到了第2层)。
第1层:包含头节点、4、7、9、15、20。
第0层(最底层):包含所有元素:1、4、7、9、12、15、18、20。
Level 3: head -------------------------------------------> 9 -------> null
Level 2: head ---------------------------> 7 -------> 9 -------> 20 -----> null
Level 1: head -------> 4 -------> 7 -------> 9 -------> 15 -----> 20 -----> null
Level 0: head -> 1 -> 4 -> 7 -> 9 -> 12 -> 15 -> 18 -> 20 -> null
说明:
1.每一层都是一个有序链表
2.每个节点在插入时随机决定其层数(比如抛硬币,直到出现反面为止,层数即连续正面的次数),因此每个节点出现在第0层,然后有1/2的概率出现在第1层,1/4的概率出现在第2层,以此类推
3.查找时从最高层开始,如果当前节点的下一个节点小于目标值,则继续向右;否则,下降一层继续查找。这样就能跳过大量节点,提高查找效率
例如,查找18:
- 从第3层开始:head->9,18>9,所以向右到9,但9后面是null(第3层结束),下降到第2层。
- 第2层:9后面是20,18<20,所以下降到第1层。
- 第1层:9后面是15,18>15,所以向右到15;15后面是20,18<20,所以下降到第0层。
- 第0层:15后面是18,找到
插入和删除操作需要调整前后节点的指针,并且由于是并发的,使用CAS操作来保证线程安全。
通过这种多层结构,跳表可以在平均情况下达到O(log n)的查找、插入和删除时间复杂度,并且支持高并发
当然 劣势就是需要每次新增或者删除之后 额外维护这些多层链表的指针,已空间换时间 和 安全
方案三: 显式锁控制(精细控制)(不推荐)
适用场景:需要特殊同步逻辑或使用 TreeSet 特定功能
class SafeTreeSet<E> {
private final TreeSet<E> set = new TreeSet<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void add(E e) {
lock.writeLock().lock();
try {
set.add(e);
} finally {
lock.writeLock().unlock();
}
}
public E first() {
lock.readLock().lock();
try {
return set.first();
} finally {
lock.readLock().unlock();
}
}
// 安全遍历方法
public void safeTraverse(Consumer<E> action) {
lock.readLock().lock();
try {
for (E e : set) {
action.accept(e);
}
} finally {
lock.readLock().unlock();
}
}
}
锁选择策略:
锁类型 | 适用场景 | 注意事项 |
---|---|---|
synchronized |
简单同步需求 | 自动释放,可能死锁 |
ReentrantLock |
需要超时或可中断锁 | 必须手动释放 |
ReentrantReadWriteLock |
读多写少场景 | 写锁降级需谨慎 |
StampedLock |
极高并发读场景 | 非重入锁,API更复杂 |
优点:
1.自动排序: 最大的优势,元素总是有序的
2.高效查找/插入/删除 (平均和最坏情况): 基于红黑树,核心操作的时间复杂度为 O(log n)
3.范围查询高效: 利用有序性和导航方法,查找子集、首尾元素等操作非常高效。
缺点:
1.比 HashSet 慢: 因为需要维护排序
,添加、删除和查找操作通常比 HashSet (平均 O(1)) 慢一些
2.不允许 null 元素: 如果尝试添加 null 元素,会抛出 NullPointerException (除非使用的 Comparator 显式处理了 null)
3.不是线程安全的: 和大多数集合类一样,TreeSet 不是线程安全的。如果需要在多线程环境中使用,需要使用 Collections.synchronizedSortedSet 进行包装,或者在外部进行同步控制
1.需要一个元素唯一且自动排序的集合
时
2.需要频繁地进行范围查询
(如查找某个区间内的元素)或访问首尾元素(最小/最大值)时
3.需要按非自然顺序
(自定义顺序)排序集合时(通过 Comparator)
4.需要利用导航方法
(如 floor(), ceiling(), higher(), lower())进行查找