Java 中 Set 接口(更新版)
一、Set 接口的核心特性
1. 元素唯一性
Set 中不允许存在重复元素,这是 Set 接口最显著的特点。其实现原理如下:
判断机制
- 基于 equals() 和 hashCode() 方法来确定元素是否重复
- 两个对象如果 equals() 返回 true,则它们的 hashCode() 必须相同
- 在 HashSet 中,首先比较 hash 值,再调用 equals() 方法确认
- 在 TreeSet 中,通过 compareTo() 或 Comparator 进行元素比较
添加行为
- 当尝试添加重复元素时,add() 方法会返回 false
- 元素不会被存入集合,原有元素保持不变
- 新元素会覆盖旧元素的情况仅发生在 equals() 返回 true 但属性不同的特殊情况
- 对于可变对象,修改已存储在 Set 中的元素可能导致意外行为
底层实现
- HashSet 使用哈希表(数组+链表/红黑树)结构,默认初始容量为 16,负载因子 0.75
- TreeSet 使用红黑树结构,通过比较器维护元素唯一性和排序
- LinkedHashSet 在 HashSet 基础上增加双向链表维护插入顺序
- 并发版本: CopyOnWriteArraySet 适用于读多写少的并发场景
实际应用示例
// 基本类型示例
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// 自定义对象示例
class Person {private String name;private int age;// 必须正确重写equals和hashCode方法@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(name, age);}
}Set<Person> personSet = new HashSet<>();
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
personSet.add(p1);
personSet.add(p2); // 不会添加成功,因为equals返回true// 错误示例:未重写equals和hashCode
class BadKey {String id;// 未重写关键方法
}
Set<BadKey> badSet = new HashSet<>();
badSet.add(new BadKey("1"));
badSet.add(new BadKey("1")); // 会被错误地添加为两个元素
2. 无序性
大多数 Set 实现类在元素存储顺序上有以下特点:
基本特性
- HashSet 不保证元素的存储顺序(基于哈希值的存储)
- 元素的存入顺序和取出顺序通常不一致
- 同一程序多次运行,元素的遍历顺序可能不同
- JDK 8 后,当哈希冲突较多时,链表会转换为红黑树,但顺序仍不可预测
特殊实现类
LinkedHashSet
- 继承自 HashSet
- 内部维护一个双向链表记录插入顺序
- 迭代顺序与插入顺序一致
- 适合需要保持插入顺序又需要去重的场景
- 典型应用:实现LRU缓存、记录用户操作序列
TreeSet
- 基于红黑树实现
- 元素按照自然顺序或指定的 Comparator 排序
- 保证元素处于排序状态
- 所有元素必须实现 Comparable 接口或提供 Comparator
- 典型应用:排行榜、范围查询、有序数据集
性能对比分析
实现类 | 添加/删除时间复杂度 | 查询时间复杂度 | 内存占用 | 适用场景 | 线程安全 |
---|---|---|---|---|---|
HashSet | O(1) | O(1) | 最低 | 普通去重 | 不安全 |
LinkedHashSet | O(1) | O(1) | 中等 | 需要保持插入顺序 | 不安全 |
TreeSet | O(log n) | O(log n) | 最高 | 需要排序 | 不安全 |
CopyOnWriteArraySet | O(n) | O(1) | 高 | 读多写少并发场景 | 安全 |
选择建议
- 当不需要关注元素顺序时,使用 HashSet 性能最佳
- 需要保持插入顺序时选择 LinkedHashSet(如购物车商品顺序)
- 需要排序或范围查询时选择 TreeSet(如按分数排序的学生列表)
- 并发环境下考虑 ConcurrentHashMap.newKeySet() 或 CopyOnWriteArraySet
3. 没有索引
Set 接口的访问方式与 List 有以下区别:
访问限制
- 没有提供 get(int index) 等通过索引访问元素的方法
- 不能使用传统 for 循环通过索引遍历元素
- 不能直接修改指定位置的元素
- 查找特定元素需要遍历或使用 contains() 方法
遍历方式详解
迭代器(Iterator)
- 最基础的遍历方式
- 可以在遍历时安全地删除元素
- 适用于所有 Collection 实现类
- 示例:
Iterator<String> it = set.iterator(); while(it.hasNext()) {String e = it.next();if(e.equals("remove")) {it.remove(); // 安全删除} }
增强 for 循环(foreach)
- 语法简洁
- 编译后实际转换为迭代器实现
- 遍历时不能修改集合结构(会抛出ConcurrentModificationException)
- 示例:
for(String e : set) {System.out.println(e); }
Java 8 的 forEach() 方法
- 支持Lambda表达式
- 内部使用迭代器实现
- 可读性高,适合简单操作
- 示例:
set.forEach(e -> System.out.println(e));
Stream API(Java 8+)
- 支持函数式编程
- 可以进行过滤、映射等复杂操作
- 并行处理能力
- 示例:
set.stream().filter(s -> s.length() > 3).map(String::toUpperCase).forEach(System.out::println);
完整遍历示例
Set<String> set = new HashSet<>(Arrays.asList("Apple", "Banana", "Cherry"));// 方法1:增强for循环(推荐简单遍历)
for(String element : set) {System.out.println(element);
}// 方法2:迭代器(需要删除元素时使用)
Iterator<String> it = set.iterator();
while(it.hasNext()) {String element = it.next();if(element.equals("Banana")) {it.remove(); // 安全删除}
}// 方法3:Java8 forEach
set.forEach(System.out::println);// 方法4:Stream API
set.stream().filter(s -> s.startsWith("A")).map(String::toLowerCase).forEach(System.out::println);// 方法5:并行流处理
set.parallelStream().forEach(System.out::println);
4. 允许存储 null 值
关于 null 值的处理:
不同实现类的处理
HashSet 和 LinkedHashSet
- 允许存储一个 null 值
- 尝试添加多个 null 会失败(因为元素唯一性)
- 查询时需要注意空指针异常
- 性能考虑:null 的哈希码固定为 0,可能影响哈希分布
TreeSet
- 不允许存储 null 值
- 添加 null 会抛出 NullPointerException
- 原因:排序需要调用 compareTo() 方法
- 替代方案:使用 Optional 包装或特殊标记值
应用场景分析
数据表示
- 表示"未知"或"不存在"的值
- 数据库查询结果映射时表示 NULL 字段
- 表单提交时表示用户未填写的可选字段
- 缓存系统中表示"未找到"的状态
特殊处理
Set<String> set = new HashSet<>();
set.add(null); // 允许
set.contains(null); // 需要显式检查// 安全处理示例
if(set.contains(null)) {// 特殊处理逻辑System.out.println("Set contains null value");
}// TreeSet 示例(会抛出异常)
try {TreeSet<String> treeSet = new TreeSet<>();treeSet.add(null); // 抛出 NullPointerException
} catch (NullPointerException e) {System.out.println("TreeSet cannot contain null");
}
注意事项
并发环境
- Collections.synchronizedSet() 包装后的 Set 对 null 的支持不变
- 并发修改可能导致不确定行为
- 建议使用 ConcurrentHashMap.newKeySet() 实现线程安全的Set
序列化
- 包含 null 的 Set 可以正常序列化/反序列化
- 自定义序列化时需要特殊处理 null 值
- JSON序列化库可能有不同的null处理方式
性能影响
- null 值的哈希码固定为 0
- 大量 null 值可能影响哈希表性能
- 考虑使用 Optional.empty() 或特殊常量替代null
框架集成
- JPA/Hibernate 等ORM框架有特殊处理
- JSON序列化库可能有不同行为
- 与其他系统交互时需明确约定 null 的含义
- REST API 中可能需要特殊处理null值的序列化
最佳实践
- 明确区分"不存在"和"空值"的概念
- 考虑使用 Optional 包装可能为null的值
- 在文档中明确说明Set对null值的处理策略
- 在团队内部建立统一的null值处理规范
- 对于TreeSet,提前过滤或转换null值
二、Set 接口的常用方法
Set 接口是 Java 集合框架的重要成员,它继承了 Collection 接口的所有方法,同时没有添加新的方法。Set 接口的核心特性是保证元素的唯一性,以下是 Set 接口中常用的方法及其详细说明:
1. boolean add(E e)
作用
向集合中添加元素,确保集合中元素的唯一性
详细说明
返回值:
- 如果元素不存在则添加成功并返回 true
- 如果元素已存在则返回 false(不添加重复元素)
底层实现机制:
- 在 HashSet 中:
- 首先计算元素的 hashCode() 确定存储位置
- 如果该位置为空,则直接添加
- 如果该位置不为空,则通过 equals() 方法比较是否相同
- 如果相同则视为重复元素,不添加
- 在 TreeSet 中:
- 通过比较器(Comparator)或自然顺序(Comparable)确定元素位置
- 如果找到相等的元素(通过 compareTo 返回0)则不添加
- 在 HashSet 中:
示例代码
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]
性能考虑
- HashSet:平均 O(1) 时间复杂度
- TreeSet:O(log n) 时间复杂度
应用场景
- 从黑名单中移除用户
- 缓存失效处理
- 动态更新数据集合
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("集合不为空");
}
性能考虑
- 时间复杂度为 O(1)
- 不需要遍历整个集合
应用场景
- 统计集合元素数量
- 检查集合是否为空
- 资源配额管理
5. boolean isEmpty()
作用
判断集合是否为空(不包含任何元素)
详细说明
- 实现原理:
- 大多数实现通过检查内部计数器是否为0
- 比 size() == 0 更高效,因为不需要计算具体数量
示例代码
Set<String> waitingList = new HashSet<>();// 初始化检查
if (waitingList.isEmpty()) {System.out.println("等待列表为空,可以开始处理");
}// 处理完成后检查
waitingList.add("user1");
waitingList.remove("user1");
if (waitingList.isEmpty()) {System.out.println("所有用户已处理完毕");
}
性能考虑
- 时间复杂度为 O(1)
- 适合在频繁检查集合是否为空的场景使用
应用场景
- 初始化检查
- 处理完成检查
- 资源状态监控
6. void clear()
作用
清空集合中的所有元素
详细说明
实现细节:
- HashSet:清空内部的哈希表数组,将各位置设为 null
- TreeSet:清空树结构,将根节点置为 null
- LinkedHashSet:清空链表和哈希表
内存影响:
- 不会缩小底层存储空间,只是清空元素
- 已分配的内存保持不变,以备后续添加元素使用
- 如果需要释放内存,应让集合对象被垃圾回收
示例代码
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
- 特点:
- 需要快速查找时使用
- 不关心元素顺序
- 基于哈希表实现,性能最佳
- 示例应用:
- 用户黑名单
- 单词计数器
- 唯一ID集合
TreeSet
- 特点:
- 需要元素排序时使用
- 元素必须实现 Comparable 或提供 Comparator
- 基于红黑树实现,保持有序
- 示例应用:
- 排行榜
- 按日期排序的事件
- 范围查询
LinkedHashSet
- 特点:
- 需要保持插入顺序时使用
- 内部维护插入顺序的链表
- 性能略低于 HashSet
- 示例应用:
- 网页访问历史记录
- 操作日志
- LRU缓存实现
重要注意事项
唯一性保证:
- Set 不允许重复元素的特点,add() 方法的行为与 List 接口有所不同
- 重复的判断基于 equals() 和 hashCode() 方法
方法实现要求:
- Set 实现通常依赖元素的 equals() 和 hashCode() 方法
- 对于自定义对象必须正确实现这两个方法
并发考虑:
- 在并发环境下应考虑使用 ConcurrentSkipListSet 或 Collections.synchronizedSet()
- 标准 Set 实现不是线程安全的
性能权衡:
- 根据需求在查找速度、顺序保持和排序功能之间进行选择
- 大型集合要考虑内存使用效率
特殊场景:
- 对于枚举类型,考虑使用 EnumSet 获得最佳性能
- 对于只读集合,可以使用 Collections.unmodifiableSet()
三、Set 接口的常用实现类
Set 接口是 Java 集合框架中的重要组成部分,它定义了不允许包含重复元素的集合。Set 接口有三个主要的实现类:HashSet、LinkedHashSet 和 TreeSet。它们在底层实现、性能和功能上各有特点,适用于不同的应用场景。
1. HashSet
基本概念
HashSet 是 Java 集合框架中 Set 接口最常用的实现类,其底层是通过 HashMap 来实现的。它实现了 Set 接口的所有方法,并提供了额外的功能。
详细特性
存储结构
HashSet 的底层实现基于哈希表,具体结构为:
- 数组 + 链表/红黑树的组合结构
- 当链表长度超过阈值(默认为8)且数组长度大于等于64时,链表会转换为红黑树
- 当红黑树节点数小于等于6时,会退化为链表
- 元素的存储位置由元素的 hashCode() 方法决定,通过哈希算法计算索引位置
元素特性
- 无序性:元素存储顺序与添加顺序无关,也不保证任何特定顺序
- 不可预测性:存储顺序可能会随着元素的添加、删除和扩容发生变化
- 唯一性:通过 equals() 和 hashCode() 方法保证元素唯一性,不允许重复元素
- 线程不安全:不是线程安全的集合
性能特点
时间复杂度:
- 添加(add):平均O(1),最坏O(n)
- 删除(remove):平均O(1),最坏O(n)
- 查找(contains):平均O(1),最坏O(n)
容量相关参数:
- 默认初始容量为16
- 默认负载因子为0.75(当元素数量达到容量*负载因子时扩容)
- 扩容时容量变为原来的2倍
特殊值处理
- 允许存储一个 null 值
- 存储 null 时会被放在哈希表的第一个位置(索引0处)
使用场景
去重应用:
- 从数据流中去除重复元素
- 统计不重复的IP地址
- 过滤重复的用户ID
快速查找:
- 黑名单/白名单检查
- 缓存系统
- 需要频繁判断元素是否存在的情况
集合运算:
- 求两个集合的并集、交集、差集
- 数据集对比分析
完整示例代码
import java.util.HashSet;
import java.util.Set;public class HashSetExample {public static void main(String[] args) {// 创建HashSetSet<String> fruits = new HashSet<>();// 添加元素fruits.add("apple");fruits.add("banana");fruits.add("orange");boolean isAdded = fruits.add("apple"); // 尝试添加重复元素System.out.println("Was apple added again? " + isAdded); // 输出false// 检查元素是否存在System.out.println("\nContains banana: " + fruits.contains("banana"));System.out.println("Contains peach: " + fruits.contains("peach"));// 遍历集合(顺序不确定)System.out.println("\nAll fruits (initial order):");for (String fruit : fruits) {System.out.println(fruit);}// 移除元素boolean isRemoved = fruits.remove("orange");System.out.println("\nWas orange removed? " + isRemoved);System.out.println("Current 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());System.out.println("Is empty? " + fruits.isEmpty());// 批量操作示例Set<String> tropicalFruits = new HashSet<>();tropicalFruits.add("mango");tropicalFruits.add("pineapple");tropicalFruits.add("banana");fruits.addAll(tropicalFruits);System.out.println("\nAfter addAll:");System.out.println(fruits);}
}
注意事项
对象相等性:
- 存储在HashSet中的对象必须正确实现equals()和hashCode()方法
- 两个对象equals()为true,则它们的hashCode()必须相同
性能调优:
- 如果预先知道元素数量,可以设置初始容量以避免多次扩容
- 对于特别大的集合,可以适当调整负载因子
线程安全:
- 多线程环境下需要使用Collections.synchronizedSet()包装
- 或者考虑使用ConcurrentHashMap.newKeySet()
迭代顺序:
- 如果需要有序迭代,可以考虑LinkedHashSet
- 如果需要排序,可以考虑TreeSet
2. LinkedHashSet
继承关系与实现原理
LinkedHashSet 是 Java 集合框架中 HashSet 的一个特殊子类,它通过继承 HashSet 并实现 Set 接口来提供有序的集合功能。其底层实现结合了两种数据结构:
哈希表结构(继承自 HashSet):
- 使用数组+链表/红黑树(JDK8+)的存储方式
- 通过元素的 hashCode() 值确定存储位置
- 保证元素的唯一性(不允许重复)
双向链表结构(新增特性):
- 维护元素之间的前驱和后继引用
- 按插入顺序记录所有元素
- 在迭代时按链表顺序遍历
核心特性对比
特性 | HashSet | LinkedHashSet | TreeSet |
---|---|---|---|
有序性 | 无 | 插入顺序 | 自然/自定义排序 |
时间复杂度 | O(1) | O(1) | O(log n) |
允许null | 是 | 是 | 否(除非指定Comparator) |
实现原理 | 哈希表 | 哈希表+链表 | 红黑树 |
性能深度分析
时间复杂度详解
- 添加操作:平均 O(1),需要同时更新哈希表和链表
- 删除操作:平均 O(1),需要解除链表节点的前后引用
- 查找操作:平均 O(1),与 HashSet 相同
- 迭代操作:O(n),直接遍历链表比 HashSet 的数组遍历更高效
内存占用
相比 HashSet 额外存储:
- 每个元素的 Entry 对象包含 before 和 after 引用
- 维护头尾指针的额外空间开销
- 通常比 HashSet 多占用 20%-30% 内存
高级应用场景
1. LRU 缓存实现
public class LRUCache<K,V> extends LinkedHashMap<K,V> {private final int capacity;public LRUCache(int capacity) {super(capacity, 0.75f, true);this.capacity = capacity;}@Overrideprotected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return size() > capacity;}
}
2. 有序去重处理
// 保持原始数据顺序的同时去重
List<Integer> numbers = Arrays.asList(3,1,4,1,5,9,2,6,5,3);
Set<Integer> uniqueNumbers = new LinkedHashSet<>(numbers);
// 输出:[3, 1, 4, 5, 9, 2, 6]
3. 访问顺序跟踪
// 记录用户访问页面的顺序
LinkedHashSet<String> visitedPages = new LinkedHashSet<>();
visitedPages.add("首页");
visitedPages.add("产品页");
visitedPages.add("购物车");
// 可以准确获取用户浏览路径
最佳实践建议
初始化容量设置:
// 预估元素数量,避免扩容 LinkedHashSet<String> set = new LinkedHashSet<>(100);
迭代性能优化:
// 使用 iterator() 比增强for循环稍快 Iterator<String> it = set.iterator(); while(it.hasNext()) {String item = it.next();// 处理逻辑 }
与ArrayList转换:
// 保持顺序转换为List List<String> list = new ArrayList<>(linkedHashSet);
并发环境替代方案:
Set<String> safeSet = Collections.synchronizedSet(new LinkedHashSet<>()); // 或使用 ConcurrentHashMap 实现的并发Set
注意事项
对象可变性:如果存储在 LinkedHashSet 中的对象修改了影响 hashCode() 的字段,会导致定位失败
Set<Person> people = new LinkedHashSet<>(); Person p = new Person("Alice"); people.add(p); p.setName("Bob"); // 危险操作!
顺序维护:以下操作会改变元素顺序:
- 先 remove() 再 add() 同一元素
- 使用 clone() 方法复制集合
- 通过序列化/反序列化恢复集合
性能监控:当元素数量极大时(>10^6),链表维护可能成为瓶颈,此时应考虑:
- 使用普通 HashSet 放弃顺序
- 采用专门的有序集合库
- 实现自定义的分片存储方案
3. TreeSet
基本概念
TreeSet 是基于 TreeMap 实现的 NavigableSet 实现类,它存储的元素按照一定的顺序排列。与 HashSet 不同,TreeSet 保证了元素的有序性,这种有序性是通过红黑树(Red-Black tree)数据结构实现的。
详细特性
存储结构
TreeSet 内部使用红黑树(一种自平衡二叉搜索树)来存储元素:
- 每个节点最多有两个子节点
- 左子节点的值小于父节点
- 右子节点的值大于父节点
- 通过自平衡机制保证树的高度相对平衡
元素排序方式:
- 自然顺序(元素实现 Comparable 接口)
- 或者通过构造时传入的 Comparator 指定排序规则
元素特性
- 自动排序:元素插入时会自动按照排序规则找到合适位置
- 元素唯一性保证机制:
- 通过 compareTo() 方法(自然排序)
- 或者 compare() 方法(自定义比较器)
- 当比较结果为 0 时视为相同元素,新元素不会被添加
- 元素比较:实际比较的是元素的内容而非内存地址
性能特点
- 添加、删除和查找操作的时间复杂度均为 O(logN)
- 相比 HashSet(O(1))和 LinkedHashSet(O(1))性能较慢
- 适合需要排序的场景,不适合单纯需要快速查找的场景
特殊值处理
- 不允许 null 值:尝试添加 null 会抛出 NullPointerException
- 原因:null 无法与其他元素进行有意义的比较
使用场景
适合场景
- 需要元素自动排序的集合
- 如成绩排名、时间排序等
- 需要范围查询的操作
- 如查找某个分数段的学生
- 需要获取子集、头部集或尾部集
- 如获取前10名或后10名的数据
不适合场景
- 需要频繁插入删除且不关心顺序
- 需要存储 null 值的集合
- 对性能要求极高的纯查找场景
完整示例代码
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); // 输出:1, 2, 5, 8}// 自定义排序示例 - 按字符串长度降序排列Comparator<String> lengthComparator = (s1, s2) -> {// 先按长度降序int lengthCompare = Integer.compare(s2.length(), s1.length());// 长度相同则按字母顺序return lengthCompare != 0 ? lengthCompare : s1.compareTo(s2);};Set<String> words = new TreeSet<>(lengthComparator);words.add("apple");words.add("banana");words.add("orange");words.add("pear");words.add("grape");words.add("kiwi");System.out.println("\nWords sorted by length (descending):");for (String word : words) {System.out.println(word); // 输出:banana, orange, apple, grape, pear, kiwi}// 特殊操作示例 - 导航方法TreeSet<Integer> scores = new TreeSet<>();scores.add(85);scores.add(92);scores.add(78);scores.add(95);scores.add(88);System.out.println("\nScores operations:");System.out.println("First (lowest): " + scores.first()); // 78System.out.println("Last (highest): " + scores.last()); // 95System.out.println("HeadSet(90): " + scores.headSet(90)); // [78, 85, 88]System.out.println("TailSet(90): " + scores.tailSet(90)); // [92, 95]System.out.println("SubSet(80, 90): " + scores.subSet(80, 90)); // [85, 88]// 尝试添加null值try {words.add(null); // 抛出NullPointerException} catch (NullPointerException e) {System.out.println("\nCannot add null to TreeSet - " + e.getMessage());}// 实际应用示例:学生成绩排名TreeSet<Student> students = new TreeSet<>(Comparator.comparing(Student::getScore).reversed().thenComparing(Student::getName));students.add(new Student("Alice", 88));students.add(new Student("Bob", 92));students.add(new Student("Charlie", 85));students.add(new Student("David", 92)); // 同分不同名students.add(new Student("Eve", 85));System.out.println("\nStudent ranking:");for (Student s : students) {System.out.println(s.getName() + ": " + s.getScore());}}static class Student {private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}public String getName() { return name; }public int getScore() { return score; }}
}
注意事项
- 线程不安全:多线程环境下需要外部同步
- 快速失败迭代器:迭代过程中修改集合会抛出 ConcurrentModificationException
- 元素可变性问题:如果排序依赖的元素属性被修改,可能导致集合行为异常
总结比较
特性 | HashSet | LinkedHashSet | TreeSet |
---|---|---|---|
底层实现 | 哈希表 | 哈希表+链表 | 红黑树 |
元素顺序 | 无序 | 插入顺序 | 排序顺序 |
性能 | O(1) | O(1) | O(logN) |
允许null | 是 | 是 | 否 |
线程安全 | 否 | 否 | 否 |
实现接口 | Set | Set | NavigableSet |
典型应用场景 | 快速查找 | 保持插入顺序 | 需要排序 |
开发者应根据具体需求选择合适的Set实现类,在不需要排序时优先考虑HashSet,需要保持插入顺序时使用LinkedHashSet,需要排序功能时选择TreeSet。
四、Set 接口使用的注意事项
equals() 和 hashCode() 方法的重写
在使用 HashSet 和 LinkedHashSet 时,如果存储的是自定义对象,必须重写对象的 equals() 方法和 hashCode() 方法,以保证元素的唯一性。重写时需要遵循以下重要规则:
- 反射性:任何非空引用值 x,x.equals(x) 应返回 true
- 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true
- 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true
- 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回相同结果
- 非空性:对于任何非空引用值 x,x.equals(null) 应返回 false
equals() 与 hashCode() 的一致性
最重要的规则是:如果两个对象通过 equals() 方法比较相等,则它们的 hashCode() 方法必须返回相同的值。但反过来不成立 - 两个对象有相同的 hashCode 值,它们不一定 equals。
示例实现(对于 Student 类):
public class Student {private String studentId;private String name;@Overridepublic boolean equals(Object o) {// 1. 检查是否是同一个对象if (this == o) return true;// 2. 检查对象是否为null或者是不同类if (o == null || getClass() != o.getClass()) return false;// 3. 类型转换Student student = (Student) o;// 4. 比较关键字段return studentId.equals(student.studentId);}@Overridepublic int hashCode() {// 使用Objects.hash()方法,确保hashCode只基于equals比较的字段return Objects.hash(studentId);}
}
哈希冲突处理
哈希冲突是指两个不同的对象具有相同的哈希码值。这是完全允许的情况,但需要注意:
- 好的哈希函数应尽量减少冲突
- 当冲突发生时,集合会使用equals()方法进行进一步比较
- 冲突会影响性能但不会影响正确性
优化哈希函数的技巧:
- 使用质数作为乘数
- 包含所有重要字段
- 避免使用可变字段
实现类选择指南
需求场景 | 推荐实现类 | 时间复杂度 | 特点 |
---|---|---|---|
快速查找/插入/删除,不关心顺序 | HashSet | O(1) | 基于哈希表实现,性能最好 |
保持插入顺序 | LinkedHashSet | O(1) | 内部维护一个双向链表记录插入顺序 |
需要自然或自定义排序 | TreeSet | O(log n) | 基于红黑树实现,自动排序,但插入和删除操作比HashSet慢 |
典型应用场景
用户ID集合(唯一性):使用HashSet存储所有注册用户的ID,确保每个用户ID唯一
Set<String> userIds = new HashSet<>();
最近访问记录(保持顺序):使用LinkedHashSet记录用户最近访问的10个页面
Set<String> recentPages = new LinkedHashSet<>(10);
成绩排名(需要排序):使用TreeSet存储学生成绩并自动排序
Set<Student> rankedStudents = new TreeSet<>(Comparator.comparing(Student::getScore).reversed());
元素的不可变性问题
问题:如果Set集合中存储的是可变对象,修改其属性可能导致:
- hashCode值变化,导致无法正常查找
- 比较结果变化,破坏TreeSet的有序性
解决方案:
- 优先使用不可变对象(如String、Integer等)
- 如果必须使用可变对象:
- 确保关键属性不会改变(设为final)
- 修改对象后从集合中移除再重新添加
- 使用防御性拷贝
示例:
Set<MutableObject> set = new HashSet<>();
MutableObject obj = new MutableObject("initial");
set.add(obj);// 错误做法:直接修改
obj.setValue("changed"); // 可能导致对象在集合中"丢失"// 正确做法:先移除再修改再添加
set.remove(obj);
obj.setValue("changed");
set.add(obj);
遍历方式
Set集合没有索引,常用遍历方式包括:
增强for循环(最简单):
for (String item : set) {System.out.println(item); }
迭代器(Iterator):
Iterator<String> it = set.iterator(); while (it.hasNext()) {System.out.println(it.next());// 可以在遍历时安全删除元素it.remove(); }
Java 8+ Stream API:
set.stream().filter(item -> item.length() > 3).forEach(System.out::println);
并行流(大数据量时):
set.parallelStream().forEach(System.out::println);
线程安全性
问题:Set接口的所有实现类(HashSet、LinkedHashSet、TreeSet)都是非线程安全的。
解决方案:
使用
Collections.synchronizedSet()
包装:Set<String> syncSet = Collections.synchronizedSet(new HashSet<>()); // 使用时需要同步块 synchronized(syncSet) {if (!syncSet.contains("key")) {syncSet.add("key");} }
使用并发集合(如
ConcurrentHashMap
的KeySet):Set<String> concurrentSet = ConcurrentHashMap.newKeySet(); // 线程安全操作 concurrentSet.add("item");
使用
CopyOnWriteArraySet
(适合读多写少场景):Set<String> copyOnWriteSet = new CopyOnWriteArraySet<>();
注意事项:
- 即使使用同步集合,复合操作(如检查再添加)仍需要额外同步
- 考虑使用
java.util.concurrent
包中的并发集合替代同步包装 - 同步集合会降低性能,只在必要时使用
TreeSet的排序问题
自然排序(元素实现Comparable接口)
- 元素类必须实现
Comparable<T>
接口 - 重写
compareTo()
方法 - 示例实现:
public class Student implements Comparable<Student> {private String studentId;private String name;@Overridepublic int compareTo(Student s) {// 按学号升序排列return this.studentId.compareTo(s.studentId);}// 注意:必须保持compareTo与equals一致@Overridepublic boolean equals(Object o) {if (this == o) return true;if (!(o instanceof Student)) return false;Student student = (Student) o;return studentId.equals(student.studentId);}@Overridepublic int hashCode() {return Objects.hash(studentId);}
}
自定义排序(通过Comparator接口)
- 创建TreeSet时指定Comparator
- 示例:
// 按年龄升序排列
TreeSet<Student> studentsByAge = new TreeSet<>(Comparator.comparingInt(Student::getAge)
);// 按姓名降序排列
TreeSet<Student> studentsByName = new TreeSet<>((s1, s2) -> s2.getName().compareTo(s1.getName())
);// 多条件排序:先按年龄,年龄相同按姓名
TreeSet<Student> students = new TreeSet<>(Comparator.comparingInt(Student::getAge).thenComparing(Student::getName)
);
唯一性规则
无论是自然排序还是自定义排序,比较结果为0的两个元素会被视为相同元素,不会被同时存入集合。这意味着:
- 比较逻辑决定了什么是"相同"元素
- 如果希望保留比较相等但实际不同的对象,需要调整比较逻辑
- 示例问题:
TreeSet<Student> set = new TreeSet<>(Comparator.comparingInt(Student::getAge));
set.add(new Student("1", "Alice", 20));
set.add(new Student("2", "Bob", 20)); // 不会被添加,因为年龄相同
解决方案是细化比较条件:
TreeSet<Student> set = new TreeSet<>(Comparator.comparingInt(Student::getAge).thenComparing(Student::getId)
);