Java 提供了多种集合类来处理数据,其中 Set 接口是一个重要的部分。它主要用于存储不重复的元素。本文将深入探讨 HashSet、LinkedHashSet 和 TreeSet 这三种实现,并详细解释它们的底层原理和使用场景。
HashSet
特性
- 无序:存储元素没有特定顺序。
- 不重复:不允许存储重复元素。
- 无索引:没有类似列表的索引功能。
底层数据结构
HashSet 底层采用哈希表(HashMap)来存储数据。具体而言,在 JDK 8 之前,HashSet 的底层数据结构是数组和链表的组合;而在 JDK 8 及以后,随着链表长度的增加,链表会转换为红黑树来提高性能。
哈希值
在了解 HashSet 的存储原理之前,首先需要理解哈希值。
- 哈希值:对象的整数表现形式。
1. 如果没有重写 hashCode 方法,不同对象计算出的哈希值是不同的。
2. 如果已经重写 hashCode 方法,不同的对象只要属性值相同,计算出的哈希值就是一样的。
3. 在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样(哈希碰撞)。
存储原理
1. 创建数组:初始化一个默认长度为 16 且加载因子为 0.75 的数组,数组名为 table。
2. 计算哈希值:根据元素的哈希值和数组长度计算出元素应存入的位置。
3. 检查位置:判断计算出的数组位置是否为 null,如果是则直接存入。
4. 处理冲突:如果位置不为 null,表示有元素存在,则调用 equals 方法比较属性值:
- 如果相同,则不存入。
- 如果不同,则存入数组,并形成链表。
JDK 8 以前,新的元素插入数组,老的元素挂在新元素下面。而 JDK 8 以后,新的元素直接挂在老元素下面,当链表长度超过 8 且数组长度大于等于 64 时,链表会自动转换为红黑树。
常见问题
1. 为什么存和取的顺序不一样?
因为 HashSet 底层使用哈希表来存储数据,哈希表并不保证顺序。
2. 为什么没有索引?
HashSet 的设计是为了快速查找,而不是按序访问,因此没有索引。
3. 如何保证数据去重?
通过 hashCode 和 equals 方法来判断元素是否重复,确保每个元素唯一。
LinkedHashSet
特性
- 有序:按照插入顺序存储元素。
- 不重复:不允许存储重复元素。
- 无索引:没有类似列表的索引功能。
底层数据结构
LinkedHashSet 基于哈希表,同时使用双向链表来维护元素的插入顺序。这意味着它既具备 HashSet 的快速查找能力,又能保留元素的插入顺序。
TreeSet
特性
- 不重复:不允许存储重复元素。
- 无索引:没有类似列表的索引功能。
- 可排序:元素可以按照自然顺序(升序)或自定义比较器进行排序。
底层数据结构
TreeSet 基于红黑树实现,红黑树是一种自平衡二叉搜索树,能够保证基本操作(如插入、删除、查找)的时间复杂度为 O(log n)。
排序方式
1. 自然排序:元素的类需要实现 Comparable 接口,并重写 compareTo 方法,指定排序规则。
@Override
//this:表示当前要添加的元素
//o:表示已经在红黑树存在的元素
//返回值:
//负数:表示当前要添加的元素是小的,存左边
//正数:表示当前要添加的元素是大的,存右边
//0 :表示当前要添加的元素已经存在,舍弃
public int compareTo(Student o) {
System.out.println("--------------");
System.out.println("this:" + this);
System.out.println("o:" + o);
//指定排序的规则
//只看年龄,我想要按照年龄的升序进行排列
return this.getAge() - o.getAge();
}
2. 比较器排序:创建 TreeSet 对象时,传入一个 Comparator 对象,指定自定义排序规则。
//排序规则:按照长度排序,如果一样长则按照首字母排序
//o1:表示当前要添加的元素
//o2:表示已经在红黑树存在的元素
//返回值规则是一样的
TreeSet<String> ts = new TreeSet<>((o1, o2)->{
// 按照长度排序
int i = o1.length() - o2.length();
// 如果一样长则按照首字母排序
i = i == 0 ? o1.compareTo(o2) : i;
return i;
});
使用场景
1. 元素可重复:使用 ArrayList,基于数组实现。
2. 增删操作多于查询:使用 LinkedList,基于链表实现。
3. 元素去重:使用 HashSet,基于哈希表实现。
4. 去重且保留顺序:使用 LinkedHashSet,基于哈希表和双向链表实现。
5. 元素排序:使用 TreeSet,基于红黑树实现。
结论
选择合适的集合类对于程序性能和功能的实现至关重要。HashSet 提供快速的元素查找和去重能力,LinkedHashSet 保留了插入顺序,TreeSet 提供了有序性和排序功能。理解每种集合的底层实现和适用场景,有助于编写高效且可靠的 Java 应用程序。