文章目录
Java集合之Set:无序不重复的元素容器
在Java集合框架中,Set
是与List
并列的重要接口,它以“无序且元素不重复”为核心特性,在数据去重、快速查找等场景中被广泛应用。本文将深入解析Set
接口及其常用实现类,帮助你理解它们的底层原理与适用场景。
一、Set接口的核心特性
Set
继承自Collection
接口,但其行为与List
有显著区别:
- 无序性:元素的存储顺序与插入顺序无关(特殊实现类如
LinkedHashSet
除外),不能通过索引访问元素。 - 唯一性/不重复:集合中不允许存在重复元素,判断重复的依据是
equals()
方法,若元素重写了equals()
,通常需同时重写hashCode()
以保证一致性。 - 无索引:没有类似
get(int index)
的方法,遍历需通过迭代器(Iterator
)或增强for循环。
Set
接口的常用方法与Collection
基本一致,如add()
、remove()
、contains()
、size()
等,核心差异体现在实现类对“去重”和“查找效率”的不同优化上。
二、常用实现类及底层原理
1. HashSet:基于哈希表的高效实现
1. JDK8以后,当链表长度超过8,而且数组长度大于等于64时,自动转换为红黑树
2. 如果集合中存储的是自定义对象,必须重写hashCode和equals方法才能用来比较属性值是否相同
在知道了HashSet的底层原理后,我们就可以回答以下三个问题了:
1. HashSet为什么存和取的顺序不一样?
因为HashSet可能是数组+链表+红黑树的结合体,而取的时候,是从最小的地址值开始遍历,而先放进去的元素的地址值不一定就小,以上都造成了HashSet的存取顺序不同。
2. HashSet为什么没有索引?
因为HashSet是数组+链表+红黑树的结合体,若存在索引,无法分配索引,难道同一个链表上和同一个红黑树上的元素都用同一个索引码,因此便取消了HashSet的索引。
3. HashSet是利用什么机制保证数据去重的?
首先利用 HashCode方法来确定元素的位置,再通过equals方法来判断属性值是否重复【重写了HashCode 和 equals 方法】。
HashSet
是Set
最常用的实现类,底层通过哈希表(HashMap) 实现,其核心特性如下:
- 存储原理:借助
HashMap
的key
存储元素(value
为固定常量),利用哈希表的特性保证元素唯一。 - 去重逻辑:添加元素时,先通过
hashCode()
计算哈希值,若哈希值不同则直接存储;若哈希值相同,再通过equals()
判断是否为同一元素,相同则拒绝添加。 - 性能:
- add()、remove()、contains()等操作的平均时间复杂度为O(1),效率极高。
- 无序性:元素存储位置由哈希值决定,与插入顺序无关。
- 注意事项:
- 元素需重写
hashCode()
和equals()
,否则可能导致重复元素存入(如自定义对象未重写方法时,默认使用地址值判断【重要的事反复强调】)。 - 线程不安全,多线程环境需手动同步(如使用
Collections.synchronizedSet()
)。
- 元素需重写
示例代码:
Set<String> hashSet = new HashSet<>();
hashSet.add("apple");
hashSet.add("banana");
hashSet.add("apple"); // 重复元素,添加失败
System.out.println(hashSet); // 输出可能为 [banana, apple](顺序不确定)
2. LinkedHashSet:保留插入顺序的哈希实现
LinkedHashSet
继承自HashSet
,底层通过LinkedHashMap实现,在哈希表基础上增加了双向链表,用于记录元素的插入顺序,因此具备以下特性:
- 有序性:遍历元素时按插入顺序返回,解决了
HashSet
的无序问题。 - 性能:
- 插入和删除效率略低于
HashSet
(因需维护链表),但遍历效率更高。 - 时间复杂度仍为O(1)(平均),与
HashSet
接近。
- 插入和删除效率略低于
- 适用场景:需要去重且保留插入顺序的场景(如日志记录、历史操作跟踪)。
示例代码:
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("first");
linkedHashSet.add("second");
linkedHashSet.add("first"); // 去重
System.out.println(linkedHashSet); // 输出 [first, second](顺序与插入一致)
3. TreeSet:基于红黑树的排序实现
TreeSet
底层通过红黑树(自平衡二叉查找树) 实现,核心特性是元素可排序:
- 两种排序方式:
- 自然排序:元素实现
Comparable
接口,重写compareTo()
方法(如Integer
、String
默认支持)。
- 自然排序:元素实现
代码示例:
public class Student implements Comparable<Student> { private String name; private int age; @Override public int compareTo(Student o) { //this:表示当前要添加的元素 //o:表示已经在红黑树存在的元素 return this.age - o.age; // 按年龄升序排列 } }
排序规则图示:
适用场景
- 元素类有明确且唯一的默认排序逻辑(如数字按大小、字符串按字典序)。
- 排序规则需要在多处复用,且不会频繁变更。
- 元素类是自定义类,允许修改其代码以实现 Comparable 接口。
定制排序 / 比较器排序:创建TreeSet
时传入Comparator
对象,自定义排序逻辑。
// 按字符串长度排序
// lambda表达式版:
TreeSet<String> ts= new TreeSet<>((o1, o2) -> o1.length() - o2.length());
// 原始版本:
TreeSet<String> ts= new TreeSet<>(new Comparator<String>() {
@Override
// o1:表示当前要添加的元素
// o2:表示已经在红黑树存在的元素
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
适用场景
- 元素类无法修改(如 String、Integer 等 JDK 内置类),无法实现 Comparable。
- 需要临时改变排序规则(同一批元素在不同场景下按不同规则排序)。
- 元素类已有默认排序,但需要覆盖默认规则(如数字默认升序,需临时按降序排列)。
- 去重逻辑:通过排序规则判断元素是否重复(
compareTo()
返回0则视为重复,而非 equals() 方法,因为其底层为红黑树,非哈希表)。 - 性能:
- add()、remove()、contains()操作的时间复杂度为O(log n),适合需要频繁排序和查找的场景。
- 遍历元素时按排序顺序返回,无需额外排序操作。
- 注意事项:
- 元素必须可比较(否则会抛出
ClassCastException
)。 - 线程不安全,多线程环境需同步处理。
- 元素必须可比较(否则会抛出
示例代码:
// 自然排序(String默认按字典序)
Set<String> treeSet = new TreeSet<>();
treeSet.add("orange");
treeSet.add("apple");
treeSet.add("banana");
System.out.println(treeSet); // 输出 [apple, banana, orange](按字典序排序)
// 定制排序(整数降序)
Set<Integer> customTreeSet = new TreeSet<>((a, b) -> b - a);
customTreeSet.add(3);
customTreeSet.add(1);
customTreeSet.add(2);
System.out.println(customTreeSet); // 输出 [3, 2, 1]
使用原则:默认使用第一种,如果第一种不能满足当前需求,就使用第二种
三、实现类对比与选择建议
三者均不重复,且无索引 哈希表 => 数组+单向链表+红黑树
实现类 | 底层结构 | 有序性 | 去重依据 | 平均时间复杂度 | 适用场景 |
---|---|---|---|---|---|
HashSet | 哈希表 | 无序 | hashCode() + equals() | O(1) | 高效去重,不关心顺序 |
LinkedHashSet | 哈希表+双向链表 | 有序,插入顺序 | hashCode() + equals() | O(1) | 去重且需保留插入顺序 |
TreeSet | 红黑树 | 可排序 | compareTo()/Comparator | O(log n) | 去重且需排序或频繁范围查询 |
四、使用注意事项
元素的不可变性:若元素是可变对象,修改其属性可能导致哈希值或排序位置变化,引发
Set
内部状态混乱(如HashSet
中元素修改后可能无法被正确查找)。建议使用不可变对象(如String
、Integer
)作为Set
元素。hashCode()与equals()的一致性:
- 若
a.equals(b) == true
,则a.hashCode()
必须等于b.hashCode()
。 - 重写时尽量保证哈希值分布均匀(减少哈希冲突),避免影响
HashSet
性能。
- 若
线程安全:所有
Set
实现类均线程不安全,多线程并发修改时需使用Collections.synchronizedSet()
包装,或直接使用ConcurrentHashMap
(JDK 1.8+)的newKeySet()
方法创建线程安全的Set
。
总结
HashSet
追求高效,LinkedHashSet
兼顾顺序,TreeSet
专注排序。理解它们的底层原理(哈希表、红黑树)和特性差异,能帮助你在实际开发中选择最合适的容器,提升代码效率与可读性。
如果我的内容对你有帮助,请 点赞 , 评论 , 收藏 。创作不易,大家的支持就是我坚持下去的动力!