一、Set接口:不重复元素的集合
1.1 Set接口的核心特性
Set接口是Collection的子接口,它具有以下核心特征:
-
唯一性:不允许重复元素(基于equals()和hashCode()判断)
-
无序性:大多数实现不保证元素的顺序
-
允许null元素:大多数实现允许一个null元素(TreeSet除外)
1.2 Set接口的方法体系
java
import java.util.*;
public class SetInterfaceDemo {
public static void main(String[] args) {
// Set没有新增方法,完全继承Collection接口
Set<String> set = new HashSet<>();
// 基本操作
set.add("Java");
set.add("Python");
set.add("Java"); // 重复元素不会被添加
System.out.println("Set大小: " + set.size()); // 2
System.out.println("是否包含Java: " + set.contains("Java")); // true
// 批量操作
Set<String> anotherSet = new HashSet<>();
anotherSet.add("C++");
anotherSet.add("JavaScript");
set.addAll(anotherSet);
System.out.println("并集后: " + set);
// 交集操作
set.retainAll(Arrays.asList("Java", "Python"));
System.out.println("交集后: " + set);
// Set的遍历(与Collection相同)
System.out.println("\n遍历Set:");
for (String language : set) {
System.out.println(language);
}
}
}
二、HashSet:基于哈希表的Set实现
2.1 HashSet的内部结构与原理
java
// HashSet的简化内部结构示意
public class HashSet<E> {
// 核心:使用HashMap存储元素
private transient HashMap<E, Object> map;
// 虚拟值,用于作为HashMap的值
private static final Object PRESENT = new Object();
// 构造函数
public HashSet() {
map = new HashMap<>(); // 默认初始容量16,负载因子0.75
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 添加元素:实际是向HashMap添加键值对
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
// 其他方法都是委托给HashMap
}
2.2 HashSet的哈希机制
java
import java.util.*;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 重写equals和hashCode是使用HashSet的关键
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
// 使用Objects.hash()自动生成hashCode
return Objects.hash(name, age);
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}
public class HashSetHashCodeDemo {
public static void main(String[] args) {
System.out.println("=== HashSet的哈希机制 ===");
// 场景1:正确重写equals和hashCode
Set<Person> personSet = new HashSet<>();
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
System.out.println("p1.equals(p2): " + p1.equals(p2)); // true
System.out.println("p1.hashCode() == p2.hashCode(): " +
(p1.hashCode() == p2.hashCode())); // true
personSet.add(p1);
personSet.add(p2); // 不会添加,因为被认为是重复元素
System.out.println("正确实现后的Set大小: " + personSet.size()); // 1
// 场景2:没有重写equals和hashCode
class BadPerson {
String name;
int age;
BadPerson(String name, int age) {
this.name = name;
this.age = age;
}
}
Set<BadPerson> badSet = new HashSet<>();
BadPerson bp1 = new BadPerson("Bob", 30);
BadPerson bp2 = new BadPerson("Bob", 30);
badSet.add(bp1);
badSet.add(bp2); // 会被添加,因为使用Object的hashCode和equals
System.out.println("\n错误实现后的Set大小: " + badSet.size()); // 2
// 场景3:哈希冲突演示
System.out.println("\n=== 哈希冲突与链表 ===");
Set<Integer> intSet = new HashSet<>();
// 查看HashMap的桶结构(通过反射)
for (int i = 0; i < 20; i++) {
intSet.add(i);
}
// 负载因子和扩容
System.out.println("HashSet的负载因子默认值: 0.75");
System.out.println("当元素数量达到容量*负载因子时,会自动扩容");
}
}
2.3 HashSet的性能特点与使用场景
java
import java.util.*;
public class HashSetPerformance {
public static void main(String[] args) {
System.out.println("=== HashSet性能分析 ===");
// 性能测试:添加、查找、删除
int size = 100000;
HashSet<Integer> hashSet = new HashSet<>(size);
// 1. 添加性能
long start = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
hashSet.add(i);
}
long addTime = System.currentTimeMillis() - start;
System.out.printf("添加%d个元素耗时: %d ms%n", size, addTime);
// 2. 查找性能
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
hashSet.contains((int) (Math.random() * size));
}
long searchTime = System.currentTimeMillis() - start;
System.out.printf("查找10000次耗时: %d ms%n", searchTime);
// 3. 删除性能
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
hashSet.remove(i);
}
long removeTime = System.currentTimeMillis() - start;
System.out.printf("删除10000个元素耗时: %d ms%n", removeTime);
// 4. 内存占用
Runtime runtime = Runtime.getRuntime();
runtime.gc();
long memoryBefore = runtime.totalMemory() - runtime.freeMemory();
HashSet<String> largeSet = new HashSet<>();
for (int i = 0; i < 100000; i++) {
largeSet.add("String" + i);
}
runtime.gc();
long memoryAfter = runtime.totalMemory() - runtime.freeMemory();
System.out.printf("\n存储100000个字符串的内存占用: %.2f MB%n",
(memoryAfter - memoryBefore) / 1024.0 / 1024.0);
}
}
三、TreeSet:基于红黑树的有序Set
3.1 TreeSet的内部结构与原理
java
// TreeSet的简化内部结构示意
public class TreeSet<E> {
// 核心:使用TreeMap存储元素
private transient NavigableMap<E, Object> m;
// 虚拟值
private static final Object PRESENT = new Object();
// 构造函数
public TreeSet() {
m = new TreeMap<>(); // 自然排序
}
public TreeSet(Comparator<? super E> comparator) {
m = new TreeMap<>(comparator); // 自定义排序
}
// 添加元素:实际是向TreeMap添加键值对
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
}
3.2 TreeSet的排序机制
java
import java.util.*;
public class TreeSetSortingDemo {
public static void main(String[] args) {
System.out.println("=== TreeSet的排序机制 ===");
// 1. 自然排序(元素必须实现Comparable接口)
TreeSet<Integer> naturalSet = new TreeSet<>();
naturalSet.add(5);
naturalSet.add(2);
naturalSet.add(8);
naturalSet.add(1);
System.out.println("自然排序: " + naturalSet); // [1, 2, 5, 8]
// 2. 自定义排序(通过Comparator)
TreeSet<String> customSet = new TreeSet<>(
(s1, s2) -> s2.compareTo(s1) // 降序
);
customSet.add("Apple");
customSet.add("Banana");
customSet.add("Cherry");
System.out.println("自定义排序(降序): " + customSet);
// 3. 对象排序
class Student implements Comparable<Student> {
String name;
int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public int compareTo(Student other) {
// 先按分数降序,再按姓名升序
if (this.score != other.score) {
return Integer.compare(other.score, this.score);
}
return this.name.compareTo(other.name);
}
@Override
public String toString() {
return name + ":" + score;
}
}
TreeSet<Student> studentSet = new TreeSet<>();
studentSet.add(new Student("Alice", 85));
studentSet.add(new Student("Bob", 92));
studentSet.add(new Student("Charlie", 85));
studentSet.add(new Student("David", 78));
System.out.println("\n学生排序:");
for (Student s : studentSet) {
System.out.println(s);
}
// 4. TreeSet的特殊方法
System.out.println("\n=== TreeSet的特殊方法 ===");
TreeSet<Integer> numbers = new TreeSet<>();
for (int i = 1; i <= 10; i++) {
numbers.add(i);
}
System.out.println("原始集合: " + numbers);
System.out.println("第一个元素: " + numbers.first()); // 1
System.out.println("最后一个元素: " + numbers.last()); // 10
System.out.println("小于等于5的最大元素: " + numbers.floor(5)); // 5
System.out.println("大于等于5的最小元素: " + numbers.ceiling(5)); // 5
System.out.println("小于5的元素: " + numbers.headSet(5)); // [1, 2, 3, 4]
System.out.println("大于等于5的元素: " + numbers.tailSet(5)); // [5, 6, 7, 8, 9, 10]
System.out.println("子集[3, 7): " + numbers.subSet(3, 7)); // [3, 4, 5, 6]
// 5. 降序视图
System.out.println("\n降序视图: " + numbers.descendingSet());
// 6. 迭代器操作
Iterator<Integer> it = numbers.descendingIterator();
System.out.print("降序遍历: ");
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
System.out.println();
}
}
3.3 TreeSet的性能分析
java
import java.util.*;
public class TreeSetPerformance {
public static void main(String[] args) {
System.out.println("=== TreeSet性能分析 ===");
int size = 100000;
// TreeSet性能测试
TreeSet<Integer> treeSet = new TreeSet<>();
// 1. 添加性能
long start = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
treeSet.add(i);
}
long treeSetAddTime = System.currentTimeMillis() - start;
// 2. 查找性能
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
treeSet.contains((int) (Math.random() * size));
}
long treeSetSearchTime = System.currentTimeMillis() - start;
// 3. 范围查询性能
start = System.currentTimeMillis();
treeSet.subSet(size/4, size*3/4);
long treeSetRangeTime = System.currentTimeMillis() - start;
// 与HashSet对比
HashSet<Integer> hashSet = new HashSet<>(size);
start = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
hashSet.add(i);
}
long hashSetAddTime = System.currentTimeMillis() - start;
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
hashSet.contains((int) (Math.random() * size));
}
long hashSetSearchTime = System.currentTimeMillis() - start;
System.out.println("\n性能对比:");
System.out.println("==========================================");
System.out.println("操作\t\t\tTreeSet\t\tHashSet");
System.out.println("==========================================");
System.out.printf("添加%d个元素\t\t%d ms\t\t%d ms%n",
size, treeSetAddTime, hashSetAddTime);
System.out.printf("查找10000次\t\t%d ms\t\t%d ms%n",
treeSetSearchTime, hashSetSearchTime);
System.out.printf("范围查询\t\t\t%d ms\t\t不支持%n", treeSetRangeTime);
// TreeSet的时间复杂度
System.out.println("\n时间复杂度对比:");
System.out.println("操作\t\t\tTreeSet\t\tHashSet");
System.out.println("添加\t\t\tO(log n)\tO(1)");
System.out.println("查找\t\t\tO(log n)\tO(1)");
System.out.println("删除\t\t\tO(log n)\tO(1)");
System.out.println("遍历\t\t\tO(n)\t\tO(n)");
System.out.println("范围查询\t\t\tO(log n + k)\t不支持");
}
}
四、LinkedHashSet:保持插入顺序的HashSet
4.1 LinkedHashSet的内部结构
java
// LinkedHashSet的简化内部结构示意
public class LinkedHashSet<E> extends HashSet<E> {
// 核心:继承自HashSet,但使用LinkedHashMap作为底层实现
// 构造函数
public LinkedHashSet() {
super(16, 0.75f, true); // 调用HashSet的特定构造函数
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, 0.75f, true);
}
// LinkedHashSet在迭代时保持插入顺序
}
4.2 LinkedHashSet的特点与使用
java
import java.util.*;
public class LinkedHashSetDemo {
public static void main(String[] args) {
System.out.println("=== LinkedHashSet特点演示 ===");
// 1. 保持插入顺序
LinkedHashSet<String> linkedSet = new LinkedHashSet<>();
linkedSet.add("First");
linkedSet.add("Second");
linkedSet.add("Third");
linkedSet.add("First"); // 重复,不会添加
System.out.println("LinkedHashSet遍历(保持插入顺序):");
for (String s : linkedSet) {
System.out.println(s); // 输出顺序与添加顺序相同
}
// 2. 访问顺序的影响(LRU)
System.out.println("\n=== 访问顺序LRU演示 ===");
// 创建一个按访问顺序排序的LinkedHashSet
LinkedHashSet<String> lruCache = new LinkedHashSet<>(16, 0.75f) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > 3; // 最大容量为3
}
};
// 转换为LinkedHashMap来模拟LRU缓存
LinkedHashMap<String, String> lruMap = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > 3;
}
};
lruMap.put("A", "ValueA");
lruMap.put("B", "ValueB");
lruMap.put("C", "ValueC");
System.out.println("初始状态: " + lruMap.keySet());
// 访问A,使其变为最近访问的
lruMap.get("A");
System.out.println("访问A后: " + lruMap.keySet());
// 添加新元素D,会淘汰最久未访问的B
lruMap.put("D", "ValueD");
System.out.println("添加D后: " + lruMap.keySet());
// 3. 性能对比
System.out.println("\n=== 性能特点 ===");
System.out.println("LinkedHashSet在HashSet的基础上:");
System.out.println("- 优点:保持插入顺序,迭代性能更好");
System.out.println("- 缺点:需要额外空间存储链表,插入稍慢");
System.out.println("- 时间复杂度:与HashSet相同,都是O(1)");
}
}
五、三大Set实现类的全方位对比
|-----------------|---------|---------------|-----------|
| 特性 | HashSet | LinkedHashSet | TreeSet |
| 底层实现 | HashMap | LinkedHashMap | TreeMap |
| 数据结构 | 哈希表 | 哈希表+链表 | 红黑树 |
| 排序保证 | 无 | 插入顺序 | 自然/自定义 |
| 允许null元素 | 是 | 是 | 否 |
| 时间复杂度(添加/查找/删除) | O(1) | O(1) | O(log n) |
| 内存占用 | 较低 | 中等 | 较高 |
| 线程安全 | 否 | 否 | 否 |
| 迭代性能 | 一般 | 优秀(顺序迭代) | 优秀(有序迭代) |
| 特殊功能 | 无 | 保持顺序 | 范围查询、排序操作 |
六、Set选择指南与最佳实践
选择决策流程图
开始选择Set实现
│
├─ 是否需要元素有序?
│ ├─ 是 → 选择TreeSet
│ │ ├─ 需要自然排序? → 元素实现Comparable接口
│ │ └─ 需要自定义排序? → 提供Comparator
│ └─ 否 →
│ ├─ 是否需要保持插入顺序?
│ │ ├─ 是 → 选择LinkedHashSet
│ │ └─ 否 → 选择HashSet
│ └─ 是否需要最高性能?
│ ├─ 是 → 选择HashSet
│ └─ 否 → 根据其他需求选择
└─ 是否需要null元素?
├─ 是 → 避免使用TreeSet(除非自定义Comparator支持null)
└─ 否 → 所有实现都可用
七、总结与建议
7.1 核心要点回顾
-
HashSet:
-
基于HashMap,哈希表实现
-
无序,性能最优(O(1))
-
需要正确实现hashCode和equals
-
-
TreeSet:
-
基于TreeMap,红黑树实现
-
有序(自然排序或自定义Comparator)
-
性能O(log n),支持范围查询
-
-
LinkedHashSet:
-
基于LinkedHashMap
-
保持插入顺序
-
性能接近HashSet,内存占用略高
-
7.2 选择决策表
| 需求 | 推荐实现 | 原因 |
|---|---|---|
| 快速查找,无需顺序 | HashSet | 性能最优 |
| 保持插入顺序 | LinkedHashSet | 有序且性能好 |
| 自然排序 | TreeSet | 自动排序 |
| 自定义排序 | TreeSet + Comparator | 灵活排序 |
| 枚举类型 | EnumSet | 性能极致优化 |
| 线程安全,读多写少 | CopyOnWriteArraySet | 并发安全 |
| 线程安全,读写均衡 | Collections.synchronizedSet | 通用方案 |
7.3 性能调优建议
-
HashSet/LinkedHashSet:
-
预估大小,设置初始容量避免扩容
-
合理设置负载因子(默认0.75)
-
确保hashCode分布均匀
-
-
TreeSet:
-
对于自定义对象,提供高效的Comparator
-
避免频繁的结构修改
-
-
通用建议:
-
小数据集(<1000):差异不大,按需求选择
-
大数据集:考虑内存和性能平衡
-
频繁操作:根据操作类型选择最优实现
-
7.4 常见问题解答
Q1:为什么HashSet允许null而TreeSet不允许?
A1:HashSet基于equals/hashCode,null可以参与比较;TreeSet基于比较器,null无法参与比较。
Q2:如何选择HashSet的初始容量?
A2:预估最终大小 ÷ 负载因子(默认0.75)。例如预计有1000个元素,设置初始容量为1334(1000/0.75)。
Q3:TreeSet和排序的List哪个更好?
A3:TreeSet自动维护排序,但元素唯一;List可以重复,但需要手动排序。根据是否需要去重和排序频率选择。
下篇预告:《Java集合(四):Map接口详解与HashMap、TreeMap、LinkedHashMap比较》
通过本篇学习,你应该已经掌握了Set接口的三大核心实现及其应用场景。在实际开发中,合理选择Set实现是保证程序性能的关键。记住:没有最好的数据结构,只有最适合的场景!