概述
Wend看源码-Java-集合学习(List)-CSDN博客
在上一篇文章中,我们深入探讨了Java集合框架的父类以及List集合的细节。接下来,本文将重点阐述Java中的Set集合,包括其内部的数据结构以及核心方法的详尽说明。
Set 集合
图1 java-Set类型数据结构类图
Set 接口
如图1 java-Set类型数据结构类图所示,Set接口继承自 Collection 接口,而Set集合数据类型的相关实现均需要实现Set接口。以下是Set接口的主要方法列表:
-
size:
返回集合中元素的数量(集合的基数)。如果元素数量超过Integer.MAX_VALUE
,则返回Integer.MAX_VALUE
。 -
isEmpty:
判断集合是否为空。如果集合不包含任何元素,则返回true
。 -
contains:
检查集合是否包含指定元素 -
toArray:
返回一个包含集合中所有元素的数组。如果集合对其迭代器返回元素的顺序有任何保证,那么这个方法返回的元素顺序必须与之相同。返回的数组是 “安全的”,即集合不会维护对它的引用,调用者可以自由修改返回的数组。这是数组和集合 API 之间的桥梁方法。 -
add:
如果指定元素尚未存在于集合中,则将其添加到集合(这是一个可选操作)。更正式地说,如果集合不包含元素e2
使得Objects.equals(e, e2)
为true
,则将指定元素e
添加到集合。如果集合已经包含该元素,则调用此方法不会改变集合,并返回false
。结合构造函数的限制,这确保了集合从不包含重复元素。可能会抛出UnsupportedOperationException
(如果集合不支持add
操作)、ClassCastException
(如果指定元素的类阻止其被添加到集合)、NullPointerException
(如果指定元素为null
且集合不允许null
元素)或IllegalArgumentException
(如果指定元素的某些属性阻止其被添加到集合)。 -
remove:
如果指定元素存在于集合中,则将其从集合中移除(这是一个可选操作)。更正式地说,移除一个元素e
,使得Objects.equals(o, e)
为true
(如果集合包含这样的元素)。如果集合包含该元素,则返回true
(或者等价地说,如果调用导致集合发生变化,则返回true
)。一旦调用返回,集合将不再包含该元素。可能会抛出ClassCastException
(如果指定元素的类型与集合不兼容)、NullPointerException
(如果指定元素为null
且集合不允许null
元素)或UnsupportedOperationException
(如果集合不支持remove
操作)。 -
containsAll:
如果集合包含指定集合中的所有元素,则返回true
。如果指定集合也是一个集合,此方法返回true
表示它是这个集合的子集。可能会抛出ClassCastException
(如果指定集合中一个或多个元素的类型与这个集合不兼容)、NullPointerException
(如果指定集合包含一个或多个null
元素且这个集合不允许null
元素,或者指定集合为null。
-
addAll:
将指定集合中的所有元素添加到这个集合中(如果它们尚未存在)(这是一个可选操作)。如果指定集合也是一个集合,addAll
操作实际上会修改这个集合,使其值为两个集合的并集。如果在操作过程中指定集合被修改,操作的行为是未定义的。可能会抛出UnsupportedOperationException
(如果集合不支持addAll
操作)、ClassCastException
(如果指定集合中一个元素的类阻止其被添加到这个集合)、NullPointerException
(如果指定集合包含一个或多个null
元素且这个集合不允许null
元素,或者指定集合为null
)或IllegalArgumentException
(如果指定集合中一个元素的某些属性阻止其被添加到这个集合)。 -
retainAll:
仅保留集合中包含在指定集合中的元素(这是一个可选操作)。换句话说,从集合中移除所有不在指定集合中的元素。如果指定集合也是一个集合,此操作实际上会修改这个集合,使其值为两个集合的交集。可能会抛出UnsupportedOperationException
(如果集合不支持retainAll
操作)、ClassCastException
(如果集合中一个元素的类与指定集合不兼容)、NullPointerException
(如果集合包含null
元素且指定集合不允许null
元素,或者指定集合为null
)。 -
removeAll:
从集合中移除所有包含在指定集合中的元素(这是一个可选操作)。如果指定集合也是一个集合,此操作实际上会修改这个集合,使其值为两个集合的不对称差集。可能会抛出UnsupportedOperationException
(如果集合不支持removeAll
操作)、ClassCastException
(如果集合中一个元素的类与指定集合不兼容)、NullPointerException
(如果集合包含null
元素且指定集合不允许null
元素,或者指定集合为null
)。 -
clear:
移除集合中的所有元素(这是一个可选操作)。可能会抛出UnsupportedOperationException
(如果集合不支持clear
方法)。 -
of:
返回一个包含零个元素的不可修改集合。从 Java 9 开始可用,详情可参考Unmodifiable Sets
部分。 -
copyOf:
返回一个包含给定集合元素的不可修改集合。给定集合必须不为null
,且不能包含null
元素。如果给定集合包含重复元素,会保留其中一个。如果给定集合后续被修改,返回的集合不会反映这些修改。如果给定集合是一个不可修改集合,调用copyOf
通常不会创建副本。从 Java 10 开始可用。
AbstractSet
AbstractSet
是一个抽象类,它实现了Set
接口。它位于 Java 集合框架中,主要作用是为Set
接口提供一个部分实现,作为创建自定义Set
实现类的基础。这个抽象类减少了实现Set
接口时需要编写的代码量,因为它已经实现了Set
接口中的一些通用方法,同时将某些方法定义为抽象方法,强制子类去实现这些特定的功能。
一些常见的Set
实现类,如HashSet
和TreeSet
间接继承自AbstractSet
。HashSet
继承自AbstractSet
并且实现了Set
接口。它利用哈希表来存储元素,提供了快速的添加、删除和查找操作。TreeSet
同样继承自AbstractSet
,它实现了SortedSet
接口,在AbstractSet
的基础上提供了排序功能,其元素是按照自然顺序或者自定义的比较器顺序存储的。
EnumSet
EnumSet
是一个专门用于处理枚举类型(enum
)的集合类。它是一个抽象类,提供了一系列高效的方法来操作基于枚举类型的集合。EnumSet
的实现基于位向量(bit - vector),这种实现方式使得它在存储和操作枚举集合时非常高效,特别是在判断元素是否属于集合、集合的并集、交集、差集等操作上。
内部实现原理
基于位向量的实现意味着对于一个枚举类型,EnumSet
可以使用一个整数(或多个整数,取决于枚举类型的大小)的二进制位来表示集合中的元素。每个枚举值对应一个二进制位,当该位为 1 时,表示对应的枚举值在集合中;当该位为 0 时,表示不在集合中。例如,如果有一个包含三个枚举值的枚举类型,EnumSet
可以用一个 3 位的二进制数来表示集合中的元素情况。这种实现方式使得集合操作(如添加、删除、包含判断等)可以通过位运算来高效地完成。
使用示例
以DayOfWeek
枚举为例,我们可以使用EnumSet
来表示一个工作日的集合:
import java.util.EnumSet;
enum DayOfWeek {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
public class EnumSetExample {public static void main(String[] args) {EnumSet<DayOfWeek> workDays = EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY,DayOfWeek.THURSDAY, DayOfWeek.FRIDAY);System.out.println("是否包含周三:" + workDays.contains(DayOfWeek.WEDNESDAY));}
}
优势与应用场景
-
性能优势:由于基于位向量的实现,在进行集合操作(如交集、并集、差集等)时,速度非常快。与使用普通的集合类(如
HashSet
)相比,EnumSet
在处理枚举类型的集合操作时可以提供更高的性能。 -
类型安全:键的类型被限制为枚举类型,保证了类型安全。
-
应用场景:适用于任何需要处理枚举类型集合的场景,如权限管理(将不同的权限用枚举表示,然后使用
EnumSet
来管理用户拥有的权限集合)、任务调度(将一周中的工作任务日用枚举表示,然后用EnumSet
来确定任务的执行日期集合)等。
RegularEnumSet
RegularEnumSet
使用一个长整型(long
)数组来表示集合中的元素,每个枚举常量对应数组中的一个位。这种实现方式使得 EnumSet
在添加、删除和检查元素存在性时非常高效。
-
大小限制:由于
RegularEnumSet
使用long
数组,它只能容纳最多 64 个枚举常量。如果枚举类型包含超过 64 个元素,EnumSet
会使用另一个内部类JumboEnumSet
来实现。 -
内存效率:由于
RegularEnumSet
使用位操作来存储枚举值,因此它非常节省内存。 -
不可变性:
RegularEnumSet
和EnumSet
一样,不允许存储null
值,并且所有的枚举类型必须属于同一个枚举类型。
JumboEnumSet
JumboEnumSet
是EnumSet
的一种特殊实现,用于当枚举常量数量超过特定阈值(通常是 64 个,因为long
类型的位长度为 64 位)时高效地存储和操作枚举集合。
-
基于数组的存储方式:当枚举类型的常量数量较多时,
JumboEnumSet
内部使用一个long
数组来存储枚举集合的信息。每个long
值的二进制位用于表示枚举常量是否在集合中。这与普通EnumSet
(在元素数量较少时)基于单个long
值的位向量表示有所不同。
HashSet
HashSet 是基于哈希表(实际上是一个 HashMap 实例)来存储元素的。哈希表的作用是通过一个哈希函数将元素的存储位置与元素本身关联起来,这样可以快速地进行元素的添加、删除和查找操作。
主要特点
-
不重复性:像其他 Set 接口的实现一样,HashSet 不允许有重复的元素。当试图添加一个已经存在于 HashSet 中的元素时,这个添加操作会被忽略。例如,HashSet<String> set = new HashSet<>(); set.add ("apple"); set.add ("apple"); 此时,HashSet 中只会有一个 "apple" 元素。
-
无序性:HashSet 中的元素是没有固定顺序的。这是因为元素的存储位置是由哈希函数决定的,并不是按照元素添加的先后顺序或者其他自然顺序(如数字大小、字母顺序等)来排列。例如,HashSet<Integer> numbers = new HashSet<>(); numbers.add (3); numbers.add (1); numbers.add (2); 在遍历这个 HashSet 时,元素的出现顺序可能是随机的,比如可能是 2,1,3 或者 3,2,1 等顺序。
-
高性能的操作:由于基于哈希表实现,HashSet 在添加、删除和查找元素方面通常具有较高的性能。添加一个元素时,通过计算元素的哈希值来确定其在哈希表中的存储位置,这个过程的时间复杂度通常在理想情况下接近常数时间 O (1)。查找和删除元素也是类似的,通过计算元素的哈希值快速定位到可能的存储位置,然后进行比较操作来确定元素是否存在或者进行删除。
应用场景
-
数据去重:当需要对一组数据进行去重处理时,HashSet 是一个很好的选择。例如,从一个包含重复元素的数组或者列表中获取不重复的元素集合。假设我们有一个包含多个学生姓名的列表,其中可能有重复的姓名,通过将列表中的元素添加到 HashSet 中,可以快速得到不重复的学生姓名集合。
-
快速查找元素是否存在:如果需要频繁地检查一个元素是否在一个集合中,HashSet 的高性能查找特性可以发挥作用。比如,在一个用户权限管理系统中,有一个存储了所有有效权限名称的 HashSet,当需要验证用户是否具有某个特定权限时,可以快速在这个 HashSet 中进行查找。
SequencedSet
SequencedSet
是 Java 21 新增的接口,用于表示具有明确顺序的集合。它继承了Set
的特性(元素的唯一性),同时也具备SequencedCollection
的顺序相关特性,提供了方法来访问第一个和最后一个元素,以及在集合的开头或结尾添加或删除元素,并且保证集合中的元素是唯一的。
SortedSet
SortedSet
继承自Set
接口和SequencedSet接口。在 Java 中,它代表一个按照自然顺序(例如数字的大小顺序、字符串的字典序等)或者通过提供的Comparator
进行排序的元素集合。集合中的元素是唯一的,就像普通的Set
一样,不允许有重复元素。
NavigableSet
NavigableSet
是 Java 集合框架中的一个接口,它继承自SortedSet
接口。它提供了用于导航和操作有序集合的方法,允许在集合中方便地查找元素,获取比给定元素大或小的元素,以及以正向或反向的顺序遍历集合。
-
查找操作相关方法
-
lower
:返回集合中小于指定元素的最大元素。如果不存在这样的元素,则返回null
。例如,在一个存储整数的NavigableSet
中,lower(5)
会返回小于 5 的最大整数。 -
higher
:返回集合中大于指定元素的最小元素。如果不存在这样的元素,则返回null
。 -
ceiling
:返回集合中大于或等于指定元素的最小元素。如果不存在这样的元素,则返回null
。 -
floor
:返回集合中小于或等于指定元素的最大元素。如果不存在这样的元素,则返回null
。
-
TreeSet
TreeSet 实现了 Set 接口,并且底层使用红黑树这种数据结构来存储元素。TreeSet 中的元素是有序的,具体来说,它们按照自然顺序或者通过构造器中指定的 Comparator 进行排序。以下是 TreeSet 的一些关键特性:
关键特性
-
有序性:TreeSet 中的元素按照某种排序顺序排列,默认是自然排序,也可以在创建 TreeSet 时提供一个 Comparator 来定义排序规则。
-
唯一性:TreeSet 不允许重复元素,如果尝试添加一个已经存在的元素,则添加操作不会成功。
-
非同步:TreeSet 不是线程安全的,如果多个线程同时访问 TreeSet,并且至少有一个线程修改了集合,则必须外部同步。
-
基于红黑树实现:TreeSet 底层使用红黑树,这是一种自平衡的二叉搜索树,能够保证插入、删除和查找操作的最坏情况时间复杂度为 O(log n)。
示例
import java.util.TreeSet;public class TreeSetExample {public static void main(String[] args) {TreeSet<Integer> treeSet = new TreeSet<>();treeSet.add(10);treeSet.add(5);treeSet.add(20);treeSet.add(15);System.out.println(treeSet); // 输出 [5, 10, 15, 20]// 检索第一个元素System.out.println("First: " + treeSet.first()); // 输出 5// 检索最后一个元素System.out.println("Last: " + treeSet.last()); // 输出 20// 移除一个元素treeSet.remove(10);System.out.println(treeSet); // 输出 [5, 15, 20]}
}
线程安全的Set集合
CopyOnWriteArraySet
CopyOnWriteArraySet 其内部是通过一个 CopyOnWriteArrayList 来实现的。CopyOnWrite(写时复制)是一种并发编程中的设计模式。当对集合进行修改操作(如添加、删除元素)时,它会先复制一份原有的数据,然后在新的数据副本上进行修改操作,最后将引用指向新的数据。这样做的好处是在读取数据时可以不用加锁,因为在修改数据时不会影响到正在读取数据的线程,从而实现了读写分离,提供了很好的读性能。
多线程下频繁读取场景
当在多线程环境中,集合的读取操作远远多于修改操作时,CopyOnWriteArraySet 是一个很好的选择。例如,在一个配置管理系统中,有多个线程需要读取配置信息(存储在集合中),而配置信息的修改相对较少,使用 CopyOnWriteArraySet 可以保证读取操作的高效性,同时保证在修改配置时的线程安全性。
ConcurrentSkipListSet
ConcurrentSkipListSet 是一个支持并发操作的有序集合类,它实现了 SortedSet 接口。它是基于跳表(Skip List)数据结构来实现的。跳表是一种可以高效地进行插入、删除和查找操作的有序数据结构,它通过在原始链表的基础上添加多层索引链表来提高操作效率。在并发环境下,多个线程可以同时安全地访问和修改这个集合。
主要特点
- 线程安全的并发操作:在多线程环境下,可以同时对集合进行添加、删除和查找操作,并且这些操作是线程安全的。例如,在一个有多个线程同时操作一个 ConcurrentSkipListSet 的场景中,不会出现数据不一致或者并发访问异常的情况,每个线程的操作都能正确地执行,就像操作一个普通的集合一样,只是在内部实现上通过复杂的并发控制机制来保证安全性。
- 有序性:和其他实现了 SortedSet 接口的集合一样,ConcurrentSkipListSet 中的元素是有序的。元素可以按照自然顺序(如果元素类型实现了 Comparable 接口)或者通过提供的 Comparator 来排序。例如,对于存储整数的 ConcurrentSkipListSet,元素会按照从小到大的顺序排列;如果是存储自定义对象,并且提供了 Comparator,会根据 Comparator 的规则来排序。
- 高性能的范围查询:由于其基于跳表的数据结构,在进行范围查询时(如查找大于某个值且小于另一个值的所有元素)具有较高的性能。例如,在一个存储时间戳的 ConcurrentSkipListSet 中,可以快速地查询出在某个时间段内的所有时间戳,这对于一些时间序列数据的处理非常有用。
并发环境下的有序数据存储和查询
在多线程环境下,当需要存储有序数据并且频繁进行添加、删除和范围查询操作时,ConcurrentSkipListSet 是一个很好的选择。比如,在一个分布式系统的任务调度器中,有多个线程需要添加任务(每个任务有一个优先级),同时需要根据优先级范围来查询任务,使用 ConcurrentSkipListSet 可以很好地满足这些需求。
参考文献
豆包
相关文章推荐
Wend看源码-Java-集合学习(List)-CSDN博客
Wend看源码-Java-集合学习(Queue)-CSDN博客