在 Java 中,Set 系列集合是一个非常重要的集合框架,它提供了多种实现,每个实现都具有独特的特性,适用于不同的场景。本文将详细介绍 Set 系列集合的相关知识,包括其通用特性、各种实现类及其底层原理。
一、Set 集合的通用特性
Set 集合具有以下几个显著的特点:
- 无序:这意味着元素的存储顺序和取出顺序可能不一致。当你将元素添加到 Set 集合中时,它们不会按照添加的顺序被存储,在取出时,其顺序也会有所不同。
- 不重复:Set 集合的一个重要特性是它能够自动去除重复元素。当你尝试添加一个已经存在于集合中的元素时,添加操作将不会成功。
- 无索引:Set 集合没有提供带索引的方法,因此你不能使用普通的 for 循环遍历,也不能通过索引来获取元素。
二、Set 集合的实现类
HashSet
- 特点:无序、不重复、无索引。
- 底层原理:
- HashSet 底层集合采用哈希表存储数据,哈希表对于增删改查数据性能都比较好。
- 具体实现步骤如下:
- 创建一个默认长度为 16,默认加载因子为 0.75 的数组,数组名
table
。 - 根据元素的哈希值跟数组长度计算出应存入的位置,公式为
int index = (数组长度 - 1) & 哈希值
。 - 判断当前位置是否为
null
,如果是null
直接存入。 - 如果位置不是
null
,表示有元素,则调用equals
方法比较属性值。 - 若元素的属性相同:不存储;若元素的属性不同:存入数组形成链表。
- JDK8 以前:新元素存入数组,老元素挂在新元素下面。
- JDK8 以后:新元素直接挂在老元素下面。
- JDK8 以后,当链表长度超过 8,而且数组长度大于等于 64 时,自动转换为红黑树。
- 创建一个默认长度为 16,默认加载因子为 0.75 的数组,数组名
- 哈希表组成:
- JDK8 之前:数组 + 链表。
- JDK8 开始:数组 + 链表 + 红黑树。
- 哈希值:
- 哈希值是对象的整数表现形式,根据
hashCode
方法计算出来的 int 类型的整数。 - 该方法定义在
Object
类中,所有对象都可以调用,默认使用地址值进行计算。 - 一般情况下,会重写
hashCode
方法,利用对象内部属性值计算哈希值。
- 哈希值是对象的整数表现形式,根据
- 对象的哈希值特点:
- 如果没有重写
hashCode
方法,不同对象计算出的哈希值是不同的。 - 如果已经重写
hashCode
方法,不同的对象只要属性值相同,计算出的哈希值就是一样的。 - 在小部分的情况下,不同属性值或者不同地址值计算出来的哈希值也有可能一样(哈希碰撞)。
- 如果没有重写
LinkedHashSet
- 特点:有序、不重复、无索引。这里的有序指的是保证存储和取出的元素顺序一致。
- 底层原理:底层数据结构依然是哈希表,只是每个元素又额外多了一个双链表的机制记录存储的顺序。
TreeSet
- 特点:不重复、无索引、可排序。
- 可排序原理:TreeSet 集合底层基于红黑树的数据结构实现排序,因此在增删改查性能上表现较好。
- 排序规则:
- 对于数据类型:如
Integer
,Double
,默认按照从小到大的顺序进行排序。 - 对于字符、字符串类型:按照字符在 ASCII 码表中的数字升序排序。
- 两种比较方式:
- 默认排序 / 自然排序:Javabean 类实现
Comparable
接口指定比较规则。
- 默认排序 / 自然排序:Javabean 类实现
- 对于数据类型:如
@Override
public int compareTo(Student o) {
return this.getAge() - o.getAge();
}
/*
this:表示当前要添加的元素
o:表示已经在红黑树存在的元素
返回值:
负数:认为要添加的元素是小,存左边
正数:认为要添加的元素是大,存右边
0:认为要添加的元素已经存在,舍弃
*/
- 比较器排序:创建 TreeSet 对象时,传递比较器
Comparator
。以下是一个使用 Lambda 表达式的示例:
public static void main(String[] args) {
// 长度排序如果长度一样则按照首字母排序
// o1 表示:当前要添加的元素
// o2 表示:已经在红黑树存在的元素
TreeSet<String> ts = new TreeSet<>((o1, o2) -> {
int i = o1.length() - o2.length();
i = i == 0? o1.compareTo(o2) : i;
return i;
});
ts.add("c");
ts.add("cffffffffffff");
ts.add("bu");
ts.add("cfm");
System.out.println(ts);
}
三、Set 集合的使用示例
以下是一个使用 Set 系列集合的简单示例,展示了如何添加元素以及使用不同的遍历方式:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SetExample {
public static void main(String[] args) {
// 1. 创建一个 Set 集合的对象
Set<String> s = new HashSet<>();
// 2. 添加元素
// 如果当前元素是第一次增加,那么可以添加成功,返回 true
// 如果当前元素是第二次添加,那么添加失败,返回 false
s.add("张三");
s.add("张三");
s.add("王五");
s.add("李四");
// 3. 打印集合
// 无序
// System.out.println(s); // [李四, 张三, 王五]
// 迭代器遍历
Iterator<String> it = s.iterator();
while (it.hasNext()) {
String str = it.next();
System.out.println(str);
}
// 增强 for 循环遍历
for (String str : s) {
System.out.println(str);
}
// Lambda 表达式遍历
s.forEach(str -> System.out.println(str));
}
}
在上述代码中,我们首先创建了一个 HashSet
对象,然后添加了一些元素。需要注意的是,由于 Set 集合不允许重复元素,当添加重复元素时,第二次添加操作将不会成功。
我们还展示了三种不同的遍历方式:
- 迭代器遍历:通过
iterator()
方法获取迭代器,使用hasNext()
方法判断是否还有元素,使用next()
方法获取下一个元素。 - 增强 for 循环遍历:这是一种简洁的遍历方式,适用于遍历实现了
Iterable
接口的集合。 - Lambda 表达式遍历:使用
forEach()
方法和 Lambda 表达式,可以更加简洁地实现元素的遍历。
四、总结
- HashSet:是一个通用的 Set 实现,当不需要保证元素的存储和取出顺序,且需要高效的元素添加、删除和查找操作时,使用 HashSet 是一个不错的选择。但如果存储自定义对象,务必重写
hashCode
和equals
方法。 - LinkedHashSet:当需要保证元素的存储和取出顺序,同时需要元素去重时,LinkedHashSet 是一个很好的选择,它在保证元素顺序方面具有优势。
- TreeSet:当需要对元素进行排序时,可使用 TreeSet。根据不同的需求,可以使用自然排序(通过实现
Comparable
接口)或比较器排序(通过传递Comparator
)来定义元素的排序规则。
以上就是对 Set 系列集合的全面介绍,通过理解其特性和底层原理,我们可以根据具体的需求选择合适的 Set 实现类,并正确地使用它们。
标签:Set,系列,元素,添加,哈希,集合,排序 From: https://blog.csdn.net/2201_75813105/article/details/145096944