Java 中 Set 接口详解:知识点与注意事项
一、Set 接口的核心特性
Set 接口作为 Collection 的子接口,具有以下核心特性:
1. 元素唯一性
Set 中不允许存在重复元素,这是 Set 接口最显著的特点。其实现原理如下:
- 判断元素是否重复是基于 equals() 和 hashCode() 方法
- 当尝试添加重复元素时,add() 方法会返回 false,且元素不会被存入集合
- 底层通过哈希表(HashSet)或红黑树(TreeSet)等数据结构来确保唯一性
实际应用示例:
Set<String> set = new HashSet<>();
System.out.println(set.add("A")); // 输出 true
System.out.println(set.add("A")); // 输出 false
System.out.println(set.size()); // 输出 1
2. 无序性
大多数 Set 实现类在元素存储顺序上有以下特点:
- HashSet 不保证元素的存储顺序(基于哈希值的存储)
- 元素的存入顺序和取出顺序可能不一致
- 例外情况:
- LinkedHashSet 会维护元素的插入顺序(双向链表实现)
- TreeSet 会根据比较器(Comparator)或自然顺序维护排序顺序
性能对比:
- 当不需要关注元素顺序时,使用 HashSet 性能最佳(O(1)时间复杂度)
- 需要保持插入顺序时选择 LinkedHashSet(稍慢于 HashSet)
- 需要排序时选择 TreeSet(O(log n)时间复杂度)
3. 没有索引
Set 接口的访问方式与 List 有以下区别:
- 没有提供 get(int index) 等通过索引访问元素的方法
- 不能使用传统 for 循环通过索引遍历元素
- 遍历方式:
- 迭代器(Iterator)
- 增强 for 循环(foreach)
- Java 8 的 forEach() 方法
遍历示例:
// 方法1:增强for循环
for(String element : set) {System.out.println(element);
}// 方法2:迭代器
Iterator<String> it = set.iterator();
while(it.hasNext()) {System.out.println(it.next());
}// 方法3:Java8 forEach
set.forEach(System.out::println);
4. 允许存储 null 值
关于 null 值的处理:
- HashSet 和 LinkedHashSet:
- 允许存储一个 null 值
- 不能存储多个 null 值(因为元素具有唯一性)
- TreeSet:
- 不允许存储 null 值(需要调用 compareTo() 方法)
- 会抛出 NullPointerException
应用场景:
- 当需要表示"未知"或"不存在"的值时可以使用 null
- 数据库查询结果映射时,可用 null 表示字段值为 NULL
- 在数据校验时,可用 null 表示缺失的必填字段
注意事项:
- 操作包含 null 的 Set 时要注意空指针异常
- 在并发环境下,Collections.synchronizedSet() 包装后的 Set 对 null 的支持与原始实现一致
二、Set 接口的常用方法
Set 接口继承了 Collection 接口的所有方法,同时没有添加新的方法。Set 接口的核心特性是保证元素的唯一性,以下是 Set 接口中常用的方法及其详细说明:
1. boolean add(E e)
作用:向集合中添加元素
返回值:
- 如果元素不存在则添加成功并返回 true
- 如果元素已存在则返回 false(不添加重复元素)
底层实现机制:
- 在
HashSet
中:- 首先计算元素的
hashCode()
确定存储位置 - 如果该位置为空,则直接添加
- 如果该位置不为空,则通过
equals()
方法比较是否相同 - 如果相同则视为重复元素,不添加
- 首先计算元素的
- 在
TreeSet
中:- 通过比较器(Comparator)或自然顺序(Comparable)确定元素位置
- 如果找到相等的元素(通过 compareTo 返回0)则不添加
示例:
Set<String> set = new HashSet<>();
boolean added1 = set.add("apple"); // 返回 true
boolean added2 = set.add("apple"); // 返回 false,因为元素已存在
System.out.println(set); // 输出: [apple]
性能考虑:
- HashSet:平均 O(1) 时间复杂度
- TreeSet:O(log n) 时间复杂度
2. boolean remove(Object o)
作用:从集合中移除指定元素
返回值:
- 如果元素存在则移除成功并返回 true
- 如果元素不存在则返回 false
注意事项:
- 对于自定义对象,必须正确重写
equals()
和hashCode()
方法才能正确移除 - 在
TreeSet
中,元素必须实现Comparable
接口或提供Comparator
- 移除操作可能触发集合内部结构的重新调整
示例:
Set<String> fruits = new HashSet<>(Arrays.asList("apple", "banana", "orange"));
boolean removed1 = fruits.remove("apple"); // 返回 true
boolean removed2 = fruits.remove("pear"); // 返回 false
System.out.println(fruits); // 输出: [banana, orange]
3. boolean contains(Object o)
作用:判断集合中是否包含指定元素
返回值:
- 包含则返回 true
- 不包含则返回 false
时间复杂度:
HashSet
:平均 O(1),最坏情况 O(n)(哈希冲突严重时)TreeSet
:O(log n)
应用场景:
- 数据去重时检查元素是否存在
- 权限验证时检查用户是否拥有某权限
- 缓存系统中检查键是否存在
示例:
Set<String> blacklist = new HashSet<>(Arrays.asList("spam", "fraud", "scam"));
if (!blacklist.contains("legit")) {System.out.println("允许访问");
}// 实现不重复添加的常用模式
Set<String> uniqueNames = new HashSet<>();
if (!uniqueNames.contains("John")) {uniqueNames.add("John");
}
4. int size()
作用:返回集合中元素的个数
注意事项:
- 对于超大型集合(超过 Integer.MAX_VALUE 元素),size() 方法可能返回 Integer.MAX_VALUE
- 不同于数组的 length 属性,size() 是方法调用
- 在并发环境下,size() 的结果可能不是精确的瞬时值
示例:
Set<Integer> primeNumbers = new HashSet<>(Arrays.asList(2, 3, 5, 7, 11, 13));
int count = primeNumbers.size();
System.out.println("集合包含 " + count + " 个质数"); // 输出: 集合包含 6 个质数// 与空集合比较
if (primeNumbers.size() > 0) {System.out.println("集合不为空");
}
5. boolean isEmpty()
作用:判断集合是否为空(不包含任何元素)
实现原理:
- 大多数实现通过检查内部计数器是否为0
- 比
size() == 0
更高效,因为不需要计算具体数量
性能考虑:
- 时间复杂度为 O(1)
- 适合在频繁检查集合是否为空的场景使用
示例:
Set<String> waitingList = new HashSet<>();// 初始化检查
if (waitingList.isEmpty()) {System.out.println("等待列表为空,可以开始处理");
}// 处理完成后检查
waitingList.add("user1");
waitingList.remove("user1");
if (waitingList.isEmpty()) {System.out.println("所有用户已处理完毕");
}
6. void clear()
作用:清空集合中的所有元素
实现细节:
HashSet
:清空内部的哈希表数组,将各位置设为 nullTreeSet
:清空树结构,将根节点置为 nullLinkedHashSet
:清空链表和哈希表
内存影响:
- 不会缩小底层存储空间,只是清空元素
- 已分配的内存保持不变,以备后续添加元素使用
- 如果需要释放内存,应让集合对象被垃圾回收
示例:
Set<String> sessionTokens = new HashSet<>();
sessionTokens.add("token1");
sessionTokens.add("token2");System.out.println("当前会话数: " + sessionTokens.size()); // 输出: 2
sessionTokens.clear();
System.out.println("清空后会话数: " + sessionTokens.size()); // 输出: 0
7. Iterator<E> iterator()
作用:返回一个迭代器,用于遍历集合中的元素
遍历特性:
HashSet
:无序遍历(基于哈希值的顺序,不稳定)LinkedHashSet
:按插入顺序遍历TreeSet
:按元素自然顺序或比较器顺序遍历
fail-fast机制:
- 如果在迭代过程中(除了通过迭代器自身的 remove 方法)修改集合,会抛出
ConcurrentModificationException
- 这是为了确保遍历时集合状态的一致性
示例:
Set<String> colors = new TreeSet<>(Arrays.asList("red", "green", "blue"));// 基本遍历
Iterator<String> it = colors.iterator();
while (it.hasNext()) {String color = it.next();System.out.println("颜色: " + color);
}// 使用for-each循环(底层也是使用迭代器)
for (String color : colors) {System.out.println("处理颜色: " + color);
}// 安全删除当前元素
Iterator<String> iterator = colors.iterator();
while (iterator.hasNext()) {String color = iterator.next();if (color.equals("green")) {iterator.remove(); // 安全删除方式}
}
选择适当的 Set 实现
这些方法构成了 Set 接口的核心功能,使得 Set 能够高效地管理不重复元素的集合。在实际开发中,根据具体需求可以选择不同的 Set 实现:
HashSet:
- 需要快速查找时使用
- 不关心元素顺序
- 基于哈希表实现,性能最佳
- 示例:用户黑名单、单词计数器
TreeSet:
- 需要元素排序时使用
- 元素必须实现 Comparable 或提供 Comparator
- 基于红黑树实现,保持有序
- 示例:排行榜、按日期排序的事件
LinkedHashSet:
- 需要保持插入顺序时使用
- 内部维护插入顺序的链表
- 性能略低于 HashSet
- 示例:网页访问历史记录、操作日志
重要注意事项:
- Set 不允许重复元素的特点,
add()
方法的行为与 List 接口有所不同 - Set 实现通常依赖元素的
equals()
和hashCode()
方法 - 对于自定义对象必须正确实现这两个方法
- 在并发环境下应考虑使用
ConcurrentSkipListSet
或Collections.synchronizedSet()
三、Set 接口的常用实现类
Set 接口是 Java 集合框架中的重要组成部分,它定义了不允许包含重复元素的集合。Set 接口有三个主要的实现类:HashSet、LinkedHashSet 和 TreeSet。它们在底层实现、性能和功能上各有特点,适用于不同的应用场景。
1. HashSet
HashSet 是 Set 接口最常用的实现类,其底层是通过 HashMap 来实现的。它具有以下特点:
详细特性
存储结构:
- 基于哈希表实现(数组 + 链表/红黑树)
- 当链表长度超过阈值(默认为8)时,链表会转换为红黑树
- 元素的存储位置由元素的 hashCode() 方法决定
元素特性:
- 无序存储,不保证元素的迭代顺序
- 存储顺序可能会随着元素的添加、删除和扩容发生变化
- 通过 equals() 和 hashCode() 方法保证元素唯一性
性能特点:
- 添加、删除和查找操作的平均时间复杂度为 O(1)
- 性能受初始容量和负载因子影响
- 默认初始容量为16,负载因子为0.75
特殊值处理:
- 允许存储一个 null 值
使用场景
- 需要快速查找元素的集合
- 不关心元素存储顺序的应用
- 需要去重的数据集合
完整示例代码
import java.util.HashSet;
import java.util.Set;public class HashSetExample {public static void main(String[] args) {Set<String> fruits = new HashSet<>();// 添加元素fruits.add("apple");fruits.add("banana");fruits.add("orange");fruits.add("apple"); // 重复元素不会添加// 检查元素是否存在System.out.println("Contains banana: " + fruits.contains("banana"));// 遍历集合(顺序不确定)System.out.println("\nAll fruits:");for (String fruit : fruits) {System.out.println(fruit);}// 移除元素fruits.remove("orange");System.out.println("\nAfter removing orange, size: " + fruits.size());// 添加null值fruits.add(null);System.out.println("\nAfter adding null:");for (String fruit : fruits) {System.out.println(fruit);}// 清空集合fruits.clear();System.out.println("\nAfter clearing, size: " + fruits.size());}
}
2. LinkedHashSet
LinkedHashSet 是 HashSet 的子类,它在 HashSet 的基础上,通过链表维护了元素的存储顺序。
详细特性
存储结构:
- 继承自 HashSet
- 内部使用哈希表+双向链表结构
- 哈希表保证元素唯一性
- 双向链表维护插入顺序
元素特性:
- 保证元素的插入顺序(FIFO)
- 迭代顺序与添加顺序一致
- 同样通过 equals() 和 hashCode() 保证唯一性
性能特点:
- 性能略低于 HashSet(需要维护链表)
- 添加、删除和查找操作的平均时间复杂度仍为 O(1)
- 比TreeSet性能更好
特殊值处理:
- 允许存储一个 null 值
使用场景
- 需要维护插入顺序的集合
- 需要快速查找且保持顺序的场景
- LRU缓存实现的基础数据结构
完整示例代码
import java.util.LinkedHashSet;
import java.util.Set;public class LinkedHashSetExample {public static void main(String[] args) {Set<String> cities = new LinkedHashSet<>();// 添加元素cities.add("Beijing");cities.add("Shanghai");cities.add("Guangzhou");cities.add("Shenzhen");cities.add("Beijing"); // 重复元素// 遍历集合(保持插入顺序)System.out.println("Cities in insertion order:");for (String city : cities) {System.out.println(city);}// 访问元素不会改变顺序System.out.println("\nContains Shanghai: " + cities.contains("Shanghai"));// 移除并重新添加元素会改变顺序cities.remove("Guangzhou");cities.add("Guangzhou");System.out.println("\nAfter modification:");for (String city : cities) {System.out.println(city);}// 性能测试long startTime = System.nanoTime();for (int i = 0; i < 10000; i++) {cities.add("City" + i);}long endTime = System.nanoTime();System.out.println("\nTime to add 10000 elements: " + (endTime - startTime) + " ns");}
}
3. TreeSet
TreeSet 是基于 TreeMap 实现的 NavigableSet 实现类,元素按照一定的顺序排列。
详细特性
存储结构:
- 基于红黑树(自平衡二叉搜索树)
- 元素按照自然顺序或指定比较器排序
元素特性:
- 元素自动排序
- 通过比较器或 Comparable 接口保证元素唯一性
- compareTo() 或 compare() 返回0视为相同元素
性能特点:
- 添加、删除和查找操作的时间复杂度为 O(logN)
- 比HashSet和LinkedHashSet慢
- 适合需要排序的场景
特殊值处理:
- 不允许null值(会抛出NullPointerException)
使用场景
- 需要元素自动排序的集合
- 需要范围查询的操作
- 需要获取子集、头部集或尾部集
完整示例代码
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;public class TreeSetExample {public static void main(String[] args) {// 自然排序示例Set<Integer> numbers = new TreeSet<>();numbers.add(5);numbers.add(2);numbers.add(8);numbers.add(1);numbers.add(5); // 重复元素System.out.println("Numbers in natural order:");for (Integer num : numbers) {System.out.println(num);}// 自定义排序示例Comparator<String> lengthComparator = (s1, s2) -> s2.length() - s1.length();Set<String> words = new TreeSet<>(lengthComparator);words.add("apple");words.add("banana");words.add("orange");words.add("pear");words.add("grape");System.out.println("\nWords sorted by length (descending):");for (String word : words) {System.out.println(word);}// 特殊操作示例TreeSet<Integer> scores = new TreeSet<>();scores.add(85);scores.add(92);scores.add(78);scores.add(95);scores.add(88);System.out.println("\nScores:");System.out.println("First: " + scores.first());System.out.println("Last: " + scores.last());System.out.println("HeadSet(90): " + scores.headSet(90));System.out.println("TailSet(90): " + scores.tailSet(90));// 尝试添加null值try {words.add(null); // 抛出NullPointerException} catch (NullPointerException e) {System.out.println("\nCannot add null to TreeSet");}}
}
总结比较
特性 | HashSet | LinkedHashSet | TreeSet |
---|---|---|---|
底层实现 | 哈希表 | 哈希表+链表 | 红黑树 |
元素顺序 | 无序 | 插入顺序 | 排序顺序 |
性能 | O(1) | O(1) | O(logN) |
允许null | 是 | 是 | 否 |
线程安全 | 否 | 否 | 否 |
实现接口 | Set | Set | NavigableSet |
典型应用场景 | 快速查找 | 保持插入顺序 | 需要排序 |
开发者应根据具体需求选择合适的Set实现类,在不需要排序时优先考虑HashSet,需要保持插入顺序时使用LinkedHashSet,需要排序功能时选择TreeSet。
四、Set 接口使用的注意事项
重写 equals() 和 hashCode() 方法
在使用 HashSet 和 LinkedHashSet 时,如果存储的是自定义对象,必须重写对象的 equals() 方法和 hashCode() 方法,以保证元素的唯一性。重写时需要遵循以下规则:
equals() 与 hashCode() 的一致性:如果两个对象通过 equals() 方法比较相等,则它们的 hashCode() 方法必须返回相同的值。
- 示例:对于 Student 类,如果认为两个学生的学号相同即为同一个学生,则 equals() 方法应比较学号,hashCode() 也应基于学号生成
- 实现模板:
@Override public boolean equals(Object o) {if (this == o) return true;if (!(o instanceof Student)) return false;Student student = (Student) o;return studentId.equals(student.studentId); }@Override public int hashCode() {return Objects.hash(studentId); }
哈希冲突处理:如果两个对象的 hashCode() 方法返回相同的值,则它们通过 equals() 方法比较不一定相等(哈希冲突是允许的)。
实现类选择指南
需求场景 | 推荐实现类 | 时间复杂度 | 特点 |
---|---|---|---|
快速查找/插入/删除,不关心顺序 | HashSet | O(1) | 基于哈希表,性能最好 |
保持插入顺序 | LinkedHashSet | O(1) | 维护插入顺序的链表 |
需要排序 | TreeSet | O(log n) | 基于红黑树,自动排序 |
典型应用场景:
元素的不可变性
遍历方式
Set 集合没有索引,常用遍历方式:
线程安全性
TreeSet 的排序问题
- 用户ID集合(唯一性):HashSet
- 最近访问记录(保持顺序):LinkedHashSet
- 成绩排名(需要排序):TreeSet
- 问题:如果 Set 集合中存储的是可变对象,修改其属性可能导致:
- hashCode 值变化,导致无法正常查找
- 比较结果变化,破坏 TreeSet 的有序性
- 解决方案:
- 优先使用不可变对象(如 String、Integer)
- 如果必须使用可变对象:
- 确保关键属性不会改变
- 修改后从集合中移除再重新添加
增强 for 循环(最简单):
for (String item : set) {System.out.println(item); }
迭代器(Iterator):
Iterator<String> it = set.iterator(); while (it.hasNext()) {System.out.println(it.next()); }
Java 8+ Stream API:
set.stream().forEach(System.out::println);
- 问题:Set 接口的所有实现类(HashSet、LinkedHashSet、TreeSet)都是非线程安全的。
- 解决方案:
- 使用
Collections.synchronizedSet()
包装:Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
- 使用并发集合(如 ConcurrentHashMap 的 KeySet)
- 使用
- 注意事项:即使使用同步集合,复合操作(如检查再添加)仍需要额外同步。
自然排序(元素实现 Comparable 接口)
- 元素必须实现 Comparable 接口
- 重写 compareTo() 方法
- 示例:
public class Student implements Comparable<Student> {@Overridepublic int compareTo(Student s) {return this.studentId.compareTo(s.studentId);} }
自定义排序(通过 Comparator 接口)
- 创建 TreeSet 时指定比较器
- 示例:
TreeSet<Student> students = new TreeSet<>((s1, s2) -> s1.getAge() - s2.getAge() );
唯一性规则:无论是自然排序还是自定义排序,比较结果为0的两个元素会被视为相同元素,不会被同时存入集合。