文章目录
- 1.概述
- 2.HashSet
- 3.LinkedHashSet
- 4.TreeSet
- 5.选择合适的 Set 实现
- 6.总结
1.概述
Set
接口的定义非常简单。它本质上是一个 Collection
,但是要求该集合不能有重复的元素。换句话说,如果尝试将一个元素添加到 Set
中,而该元素已经存在于 Set
中,那么 add
方法将返回 false
,并且 Set
本身不会改变。
Java为 Set
接口提供了几个主要的实现:
HashSet
: 基于哈希表的Set
实现,它不保证集合的迭代顺序;尤其是它不保证该顺序恒久不变。LinkedHashSet
: 哈希表和链表实现的HashSet
,具有可预知迭代顺序。TreeSet
: 基于树(红黑树)的Set
实现,按照元素的自然顺序进行排序,或者根据创建集合时提供的比较器进行排序。
2.HashSet
Java中的
Set
接口有几个实现,但是最常用的无疑是HashSet
。我们经常默认地使用它。
HashSet
是 Set
接口的一种基础实现,被广泛应用于各种程序中。
其类图如下:
根据其源码声名,我们可以看到 HashSet
实际上是由 HashMap
支持的,HashMap
则是基于哈希表的实现。这种数据结构设计,使得 HashSet
具有出色的存取和查找性能。
哈希表是一种数据结构,它提供了快速的元素插入和查找操作。在 HashSet
中,通过哈希算法来确定元素在哈希表中的位置。这意味着,无论 HashSet
中有多少元素,确定元素是否存在(以及获取这个元素)的时间大致是常数 O(1) 的,这是 HashSet
高效性能的主要来源。
HashSet
确定两个元素相等的标准是:两个对象的 hashCode()
方法返回值相等,并且两个对象的 equals()
方法返回结果也相等。hashCode()
方法用于确定元素在哈希表中的位置,而 equals()
方法则用于在哈希冲突时比较元素的实际值。这意味着,如果你要将自己的对象存储在 HashSet
中,那么你应该重写这两个方法,以确保它们的行为符合 HashSet
的需求。
下面是一个简单的重写 hashCode()
和 equals()
方法的案例:
public class MyDate {
private int year;
private int month;
private int day;
@Override
public boolean equals(Object o){
System.out.println("调用equals()方法");
// 如果对象地址一样,则认为相同
if (this == o) return true;
// 如果参数为空,或者类型信息不一样,则认为不同
if (!(o instanceof MyDate)) return false;
// 转换为当前类型
MyDate myDate = (MyDate) o;
// 使用 == 比较基本类型,使用 equals 比较引用类型(此处没有必要)
return year == myDate.year && month == myDate.month && day == myDate.day;
}
@Override
public int hashCode(){
System.out.println("调用hashCode()方法");
// Objects类的hash方法返回一个int类型的值,作为哈希值
return Objects.hash(year, month, day);
}
@Override
public String toString(){
return "MyDate{" + "year=" + year + ", month=" + month + ", day=" + day + '}';
}
// 省略构造器、getter和setter方法
}
下面进行基本的测试:
public class TestHashSet {
public static void main(String[] args) {
// 创建HashSet集合
HashSet<String> set = new HashSet<>();
// 添加元素
set.add("Java");
set.add("Java"); // 重复元素
set.add("Python");
set.add("C");
// 输出集合(不保证顺序)
System.out.println(set);
// 创建HashSet集合
HashSet<MyDate> set1 = new HashSet<>();
// 添加元素
set1.add(new MyDate(2020, 1, 1));
set1.add(new MyDate(2020, 1, 1)); // 重复元素
set1.add(new MyDate(2020, 1, 2));
// 输出集合(不保证顺序)
System.out.println(set1);
}
}
输出结果分析:
调用hashCode()方法
调用hashCode()方法
调用equals()方法
调用hashCode()方法
[MyDate{year=2020, month=1, day=2}, MyDate{year=2020, month=1, day=1}]
从上面的结果来看,我们发现在执行添加操作时会自动调用 hashCode()
方法通过 Hash 算法为该元素设置一个 Hash 值以确定在哈希表中的存储位置。当添加重复元素时同样先调用 hashCode()
方法通过 Hash 算法为该元素设置一个 Hash 值,此时发现该哈希值已经存在,于是自动调用 equals()
方法进行进一步的比较,如果判定是同一个元素则不进行添加操作。
可见,通过使用 HashMap
作为其内部结构,HashSet
利用了哈希表的性能优势。不仅如此,它也遵循了一种非常强大的对象相等性检查策略。这使得 HashSet
成为 Java 集合中高效、可靠的选项,无论是在性能还是在语义上,都使得 HashSet
成为实现 Set
接口的理想选择。
3.LinkedHashSet
在Java的集合框架中,
LinkedHashSet
是一个特殊的Set
实现,它继承自HashSet
并提供了一些额外的特性。
LinkedHashSet
是 HashSet
的一个扩展子类,其类图如下:
HashSet
是基于哈希表实现的,提供了出色的元素插入和查找性能。然而,它并没有保留元素的插入顺序,这在某些场景下可能是一个缺点。这就是 LinkedHashSet
被引入的原因。它在 HashSet
的基础上,添加了两个指针域 before
和 after
,用于链接各个元素节点,从而记录了元素的添加顺序。
因此,LinkedHashSet
实际上是链表和哈希表的组合结构。链表保持了元素的插入顺序,而哈希表保证了快速的元素插入和查找性能。这种结构使得 LinkedHashSet
不仅继承了 HashSet
的高性能,同时还能提供可预测的迭代顺序。
在插入性能上,由于 LinkedHashSet
需要维护额外的链表,所以其性能略低于 HashSet
。然而,这种性能下降通常是可以接受的,特别是在需要保持插入顺序的场景下。
在迭代访问性能上,LinkedHashSet
表现得非常出色。由于它维护了一个运行在插入顺序上的链表,所以在遍历 Set
的所有元素时,LinkedHashSet
提供了高效且稳定的性能。这使得它在需要频繁迭代的应用场景下,成为一个非常好的选择。
下面是一个简单的使用案例:
public class LinkedHashSetTest {
public static void main(String[] args) {
LinkedHashSet<String> set = new LinkedHashSet<>();
// 添加元素
set.add("Java");
set.add("Java"); // 重复元素
set.add("Python");
set.add("C");
// 输出集合(保证顺序)
System.out.println(set); // [Java, Python, C]
}
}
4.TreeSet
TreeSet
是 Java 集合框架中的一个重要成员,它提供了一种以排序和去重为核心特性的集合。
TreeSet
的底层是基于 TreeMap
实现的,TreeMap
的底层数据结构是红黑树,一种自平衡的二叉搜索树。由于红黑树的性质,TreeSet
能够高效地进行元素的插入、删除和查找操作,同时保证了元素的排序。
其类图如下:
TreeSet
的两大核心特性是元素的去重和排序。
去重的逻辑主要取决于比较元素的方式。TreeSet
支持两种比较方式,分别是自然排序和定制排序。
- 对于自然排序,
TreeSet
要求集合元素实现Comparable
接口,并重写compareTo
方法。当TreeSet
在添加新元素时,会调用该元素的compareTo
方法与已有元素进行比较。如果返回值为 0,表示两个元素相等,新元素就不会被添加到TreeSet
中。 - 对于定制排序,
TreeSet
在创建时需要指定一个实现了Comparator
接口的对象。当添加新元素时,TreeSet
会调用Comparator
的compare
方法进行元素比较。同样,如果compare
方法返回0,新元素就不会被添加到TreeSet
中。
对于排序,TreeSet
支持自然排序和定制排序两种方式:
- 自然排序:
TreeSet
要求集合元素实现Comparable
接口并重写compareTo
方法。compareTo
方法的返回值决定了元素的排列顺序。 - 定制排序:创建
TreeSet
对象时,可以通过构造函数传入一个Comparator
对象。Comparator
接口中的compare
方法将被用于元素的排序。
下面是一个简单实用案例:
public class TreeSetTest {
public static void main(String[] args) {
/*
* 默认情况下采用自然排序,会调用 Comparable 接口中的 compareTo 方法进行比较
* 1.对于字符串:按照 Unicode 编码值的大小进行比
* 2.对于自定义类型:需要实现 Comparable 接口,重写 compareTo 方法
* 3.对于整形:按照数值大小进行比较
* 4.对于浮点型:按照数值大小进行比较
* 5.对于布尔型:false < true
*/
TreeSet<String> set = new TreeSet<>();
// 添加元素
set.add("Java");
set.add("Java"); // 重复元素
set.add("Python");
set.add("C");
set.add("C++");
set.add("Go");
set.add("C#");
// 输出集合
System.out.println(set); // [C, C#, C++, Go, Java, Python]
}
}
public class TreeSetTest02 {
public static void main(String[] args) {
// 如果是定制排序,需要在创建 TreeSet 时传入 Comparator 接口的实现类对象,重写 compare 方法去自定义排序规则
TreeSet<String> set = new TreeSet<>((o1, o2) -> {
// 按照字符串长度比较
return o1.length() - o2.length();
});
// 添加元素
set.add("Java");
set.add("Python");
set.add("C");
set.add("C++");
set.add("Go");
// 输出集合
System.out.println(set); // [C, Go, C++, Java, Python]
}
}
5.选择合适的 Set 实现
选择哪种 Set
实现,主要取决于你的具体需求:
- 如果你只是需要一个不包含重复元素的集合,并不关心元素的顺序,那么
HashSet
是一个很好的选择。它提供了常数时间的基本操作(add、remove 和 contains)。 - 如果你关心元素的插入顺序,那么
LinkedHashSet
是一个更好的选择。它在HashSet
的基础上,使用链表维护了元素的插入顺序。 - 如果你需要一个排序的集合,那么
TreeSet
是最好的选择。它使用红黑树来存储元素,提供了有序的集合视图。
6.总结
下面是一个简单的总结表格:
特性 | HashSet | LinkedHashSet | TreeSet |
---|---|---|---|
元素的顺序 | 无序 | 插入顺序 | 有序 |
允许 null | 是 | 是 | 是 |
性能 | 高 | 中等 | 较低 |
基于 | HashMap | LinkedHashMap | TreeMap |
特殊功能 | 无 | 记录插入顺序 | 排序 |