Java Set集合核心知识点总结(HashSet/LinkedHashSet/TreeSet/ConcurrentSkipListSet)
在Java集合框架中,Set是继承自Collection的顶级接口之一,代表元素不可重复、无索引的集合。与List允许重复元素且支持索引访问不同,Set的核心特性是唯一性,通过底层哈希表或红黑树保证元素不重复。本文将从底层原理、源码解析、性能对比、面试陷阱等维度深入梳理Set的四大核心实现类,帮你彻底掌握Set集合的使用与选型。
一、HashSet:基于哈希表的无序去重实现
1. 核心特点
- 底层完全基于HashMap实现,所有元素存储在HashMap的key中,value是一个固定的静态Object常量
- 无序:不保证元素的插入顺序,也不保证顺序随时间不变
- 元素不可重复:通过key的唯一性保证,重复元素添加失败
- 允许一个null元素(因为HashMap允许一个null键)
- 线程不安全:多线程环境下操作会出现并发修改异常
- 综合性能最高,是日常开发中最常用的Set实现
2. 底层原理(重点)
HashSet的本质是一个"阉割版"的HashMap,所有核心操作都委托给HashMap完成:
java
// HashSet的核心成员变量
private transient HashMap<E,Object> map;
// 用于填充HashMap value的静态常量
private static final Object PRESENT = new Object();
// 无参构造器:创建默认容量16、负载因子0.75的HashMap
public HashSet() {
map = new HashMap<>();
}
// add方法核心实现:调用HashMap的put方法
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
- 去重原理 :当调用
add(e)时,HashMap会先计算e的hashCode定位数组索引,再通过equals()方法比较元素是否相等。如果两个元素hashCode相同且equals返回true,则视为重复元素,put方法返回旧值,add方法返回false表示添加失败。 - 扩容机制:完全继承HashMap的扩容规则,初始容量16,负载因子0.75,扩容为原容量的2倍。
3. 常用方法与示例
(1)基本操作
java
HashSet<String> set = new HashSet<>();
// 添加元素,重复元素添加失败返回false
set.add("a");
set.add("b");
set.add("a"); // 返回false,添加失败
System.out.println(set); // 输出:[a, b](顺序不定)
// 获取元素数量
System.out.println(set.size()); // 输出:2
// 判断元素是否存在
boolean hasQingcheng = set.contains("a"); // 输出:true
// 删除元素,删除成功返回true
set.remove("b"); // 输出:true
// 清空集合
set.clear();
// 判断是否为空
System.out.println(set.isEmpty()); // 输出:true
(2)遍历方式
java
// 1. 增强for循环(推荐,简洁)
for (String s : set) {
System.out.println(s);
}
// 2. 迭代器遍历(唯一支持遍历过程中安全删除的方式)
Iterator<String> it = set.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("b")) {
it.remove(); // 安全删除
}
}
// 3. Stream遍历(Java 8+)
set.stream().forEach(System.out::println);
4. 常见陷阱与注意事项
- 必须正确重写元素的hashCode()和equals()方法:这是HashSet保证元素唯一性的核心。如果只重写equals不重写hashCode,会导致相同内容的对象被视为不同元素;反之则会导致哈希冲突严重,性能下降。
- 避免使用可变对象作为Set元素:如果元素的内容发生变化,其hashCode也会改变,导致HashSet无法找到该元素,造成内存泄漏。
- 初始容量优化 :如果提前知道元素数量,创建HashSet时指定初始容量(建议
预计元素数 / 0.75 + 1),避免频繁扩容带来的性能损耗。
二、LinkedHashSet:维护插入顺序的哈希表实现
1. 核心特点
- 继承自HashSet,底层基于LinkedHashMap实现,在HashMap的数组+链表+红黑树结构基础上,额外维护了一条双向链表记录元素的插入顺序
- 有序:严格按照元素的插入顺序进行存储和遍历
- 元素不可重复:与HashSet一致,通过HashMap的key唯一性保证
- 允许一个null元素
- 线程不安全
- 性能略低于HashSet(因为要维护双向链表),但遍历性能更高(按链表顺序遍历,无需遍历整个数组)
2. 底层原理
LinkedHashSet的构造器会调用父类HashSet的特定构造器,创建一个LinkedHashMap实例:
java
// HashSet的受保护构造器,供LinkedHashSet调用
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
// LinkedHashSet无参构造器
public LinkedHashSet() {
super(16, .75f, true);
}
- LinkedHashMap的双向链表会记录每个节点的前驱和后继节点,从而保证插入顺序。
- 去重机制与HashSet完全一致,同样依赖hashCode和equals方法。
3. 常用方法与示例
LinkedHashSet的方法与HashSet完全相同,唯一区别是遍历顺序与插入顺序一致:
java
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Cherry");
set.add("Apple"); // 重复元素,添加失败
System.out.println(set); // 输出:[Apple, Banana, Cherry](严格按插入顺序)
// 集合运算:交集、差集
LinkedHashSet<String> set1 = new LinkedHashSet<>(Arrays.asList("A", "B", "C"));
LinkedHashSet<String> set2 = new LinkedHashSet<>(Arrays.asList("B", "C", "D"));
// 交集:保留两个集合共有的元素
set1.retainAll(set2);
System.out.println(set1); // 输出:[B, C]
// 差集:删除set1中与set2共有的元素
set1.removeAll(set2);
System.out.println(set1); // 输出:[]
4. 适用场景
当需要去重且保持元素的插入顺序时使用,例如:
- 记录用户的浏览历史(去重且按浏览顺序展示)
- 实现简单的LRU缓存(结合LinkedHashMap的访问顺序模式)
三、TreeSet:基于红黑树的有序去重实现
1. 核心特点
- 底层基于TreeMap实现,所有元素存储在TreeMap的key中
- 有序:支持两种排序方式------自然排序(默认)和自定义排序
- 元素不可重复:通过排序规则的比较结果为0来判断元素重复,而非hashCode和equals
- 不允许null元素(因为需要参与排序比较)
- 线程不安全
- 插入、删除、查询的时间复杂度均为O(logn),性能低于HashSet,但支持有序操作
2. 排序规则
TreeSet的排序优先级:自定义排序 > 自然排序
-
自然排序 :元素必须实现
Comparable接口,重写compareTo()方法。String、Integer等包装类已默认实现该接口:- Integer:按数值升序
- String:按字典序升序
- Date:按时间戳升序
-
自定义排序 :创建TreeSet时传入
Comparator接口的实现类,重写compare()方法,优先级高于自然排序。
3. 常用方法与示例
(1)自然排序
java
// Integer自然排序(数值升序)
TreeSet<Integer> numSet = new TreeSet<>();
numSet.add(3);
numSet.add(1);
numSet.add(2);
System.out.println(numSet); // 输出:[1, 2, 3]
// String自然排序(字典序)
TreeSet<String> strSet = new TreeSet<>();
strSet.add("agg");
strSet.add("abcd");
strSet.add("ffas");
System.out.println(strSet); // 输出:[abcd, agg, ffas]
(2)自定义排序
java
// 自定义排序:按Person的年龄升序,年龄相同按姓名字典序
TreeSet<Person> personSet = new TreeSet<>((p1, p2) -> {
if (p1.getAge() != p2.getAge()) {
return p1.getAge() - p2.getAge();
} else {
return p1.getName().compareTo(p2.getName());
}
});
personSet.add(new Person("agg", 21));
personSet.add(new Person("abcd", 12));
personSet.add(new Person("ffas", 8));
personSet.add(new Person("agg", 12));
for (Person p : personSet) {
System.out.println(p.getName() + ":" + p.getAge());
}
// 输出:
// ffas:8
// abcd:12
// agg:12
// agg:21
(3)特有有序方法
java
TreeSet<Integer> set = new TreeSet<>(Arrays.asList(1, 2, 3, 4, 5));
// 获取最小/最大元素
System.out.println(set.first()); // 输出:1
System.out.println(set.last()); // 输出:5
// 获取小于等于指定值的最大元素
System.out.println(set.floor(3)); // 输出:3
// 获取大于等于指定值的最小元素
System.out.println(set.ceiling(3)); // 输出:3
// 获取子集合:[fromElement, toElement)
SortedSet<Integer> subSet = set.subSet(2, 4);
System.out.println(subSet); // 输出:[2, 3]
// 获取小于指定值的子集合
SortedSet<Integer> headSet = set.headSet(3);
System.out.println(headSet); // 输出:[1, 2]
// 获取大于等于指定值的子集合
SortedSet<Integer> tailSet = set.tailSet(3);
System.out.println(tailSet); // 输出:[3, 4, 5]
4. 注意事项
- 排序规则必须与equals()保持一致 :如果两个元素通过
compareTo()或compare()方法比较返回0,但equals()返回false,TreeSet会认为它们是重复元素,导致其中一个无法添加。 - 不允许null元素 :因为排序时需要调用元素的
compareTo()方法,null会抛出NullPointerException。 - 性能权衡:TreeSet的插入和删除操作需要维护红黑树的平衡,性能比HashSet低约5-10倍,仅在需要有序操作时使用。
四、ConcurrentSkipListSet:高并发场景的有序Set实现
1. 核心特点
- 底层基于ConcurrentSkipListMap实现,采用跳表(Skip List)数据结构
- 有序:支持自然排序和自定义排序,与TreeSet一致
- 线程安全:通过CAS和volatile保证并发操作的安全性,无需加锁
- 元素不可重复:通过排序规则判断重复
- 不允许null元素
- 并发性能远高于
Collections.synchronizedSortedSet包装的TreeSet,是高并发场景下有序Set的唯一选择
2. 核心原理
跳表是一种基于多层索引的有序数据结构,通过空间换时间,将查询时间复杂度从O(n)优化为O(logn)。ConcurrentSkipListSet采用无锁算法,支持多个线程并发读写,读操作完全无锁,写操作仅影响局部节点,并发性能优异。
3. 适用场景
- 高并发环境下需要有序去重的场景
- 替代线程不安全的TreeSet和性能低下的
Collections.synchronizedSortedSet
五、Set集合核心总结与选型对比
1. Set接口通用特性
- 元素不可重复,通过底层机制保证唯一性
- 无索引,不支持通过索引访问元素
- 继承自Collection接口,支持集合的通用操作(添加、删除、查询、遍历)
- 所有实现类的迭代器都是快速失败(fail-fast)的,遍历过程中修改集合会抛出
ConcurrentModificationException(ConcurrentSkipListSet除外,是弱一致性迭代器)
2. 四大实现类对比表
| 特性 | HashSet | LinkedHashSet | TreeSet | ConcurrentSkipListSet |
|---|---|---|---|---|
| 底层数据结构 | HashMap(数组+链表+红黑树) | LinkedHashMap(数组+双向链表+红黑树) | TreeMap(红黑树) | ConcurrentSkipListMap(跳表) |
| 有序性 | 无序 | 插入顺序 | 排序顺序(自然/自定义) | 排序顺序(自然/自定义) |
| null元素支持 | 允许1个 | 允许1个 | 不允许 | 不允许 |
| 线程安全 | 不安全 | 不安全 | 不安全 | 安全(无锁并发) |
| 平均时间复杂度 | O(1) | O(1) | O(logn) | O(logn) |
| 插入性能 | 极高 | 高 | 中等 | 高(并发场景) |
| 遍历性能 | 中等 | 极高 | 中等 | 中等 |
| 适用场景 | 单线程通用去重场景 | 需要保持插入顺序的去重 | 需要有序排序的去重 | 高并发有序去重场景 |
| 综合性能 | 最高 | 高 | 中等 | 高(并发场景) |
3. List与Set的核心区别
| 特性 | List | Set |
|---|---|---|
| 有序性 | 严格按插入顺序排序 | 大部分实现无序(除LinkedHashSet、TreeSet) |
| 元素重复性 | 允许重复元素 | 不允许重复元素 |
| 索引支持 | 支持通过int索引访问元素 | 不支持索引访问 |
| 去重机制 | 无,需手动判断 | 底层自动保证唯一性 |
| 遍历方式 | 普通for循环、增强for、迭代器 | 增强for、迭代器、Stream |
| 常用实现类 | ArrayList、LinkedList | HashSet、LinkedHashSet、TreeSet |
| 适用场景 | 频繁查询、需要索引访问 | 去重、集合运算 |
结论
Set集合是Java开发中实现元素去重和集合运算的核心工具,掌握其底层原理和选型技巧能显著提升代码质量和运行性能。日常开发中:
- 绝大多数单线程去重场景优先使用HashSet,性能最高
- 当需要去重且保持插入顺序时,选择LinkedHashSet
- 当需要对元素进行排序时,选择TreeSet
- 在高并发场景下需要有序去重时,选择ConcurrentSkipListSet
- 永远不要使用
Collections.synchronizedSet包装的HashSet或TreeSet,性能远低于专门的并发实现