目录
先给大家看一张图,有个大致了解:
再来看下Set的继承关系:
Set实现了以下几种:
- HashSet
- TreeSet
- LinkedHashSet
- CopyOnWriteArraySet
- ConcurrentSkipListSet
一、HashSet
1. 说明
- HashSet实现了Set接口
- HashSet底层实质上是HashMap
- 可以存放null值,但是只能有一个null
- HashSet不保证元素是有序的,取决于hash后,再确定索引的结果,即不保证存放元素的顺序和取出顺序一致
- 不能有重复元素/对象
2. 底层机制说明
HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
- 先获取元素的哈希值(hashcode方法)
- 对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
- 如果该位置上没有其他元素,则直接存放,如果该位置上有其他元素,则需要进行equals判断,如果相等,则不再添加,如果不相等,则以链表的方式添加
- Java8以后,如果一条链表中的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行数化(红黑树)
3. 分析扩容和转成红黑树机制
- HashSet底层是HashMap,第一次添加时,table的数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor是0.75)=12
- 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,依次类推
- Java8以后,如果一条链表中的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行数化(红黑树),否则仍然采用数组扩容机制
二、TreeSet
1、简介
TreeSet是Java中的一个类,它实现了Set接口,并且是一个有序的,不允许存储重复元素的集合。它的底层实现是红黑树(一种自平衡的二叉查找树),因此,它的元素会自动按升序排列。
以下是TreeSet的一些主要特性:
- 有序性:TreeSet中的元素会按照自然顺序(升序)排列,或者根据创建TreeSet时提供的Comparator进行排序,具体取决于使用的构造方法。
- 不允许重复元素:TreeSet中不允许存储重复的元素,如果试图添加重复的元素,add方法会返回false,且不会改变集合的状态。
- null元素:与HashSet不同,TreeSet不允许插入null元素,否则会抛出NullPointerException。
- 非线程安全:和HashSet一样,TreeSet也不是线程安全的。如果多个线程同时修改一个TreeSet,且至少有一个线程修改了它,那么它必须保持外部同步。这通常是通过在某个自然封装了该集合的对象上进行同步来完成的。
请注意,由于TreeSet是有序的,所以它的性能相对于HashSet会稍微慢一些,特别是对于大量的数据插入操作。因此,在选择使用HashSet还是TreeSet时,需要根据实际应用的需求进行权衡。
2、排序方式
TreeSet支持两种排序方法:自然排序和定制排序。TreeSet默认采用自然排序。
什么是自然排序?
TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间大小关系,然后将集合元素按升序排列,这种方式就是自然排序。(比较的前提:两个对象的类型相同)。
什么是定制排序?
TreeSet的自然排序是根据集合元素的大小,TreeSet将他们以升序排列。如果需要实现定制排序,例如降序,则可以使用Comparator接口。该接口里包含一个int compare(T o1, T o2)方法,该方法用于比较o1和o2的大小。
如果需要实现定制排序,则需要在创建TreeSet集合对象时,并提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。
三、LinkedHashSet
1. LinkedHashSet概述:
是具有可预知迭代顺序的Set接口的哈希表和链接列表实现。此实现与HashSet的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希Set,而其中至少一个线程修改了该Set,则它必须保持外部同步。
2. LinkedHashSet的实现:
对于LinkedHashSet而言,它继承与HashSet、又基于LinkedHashMap来实现的。
LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同,因此LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个LinkedHashMap来实现,在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。LinkedHashSet的源代码如下:
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
/**
* 构造一个带有指定初始容量和加载因子的新空链接哈希set。
*
* 底层会调用父类的构造方法,构造一个有指定初始容量和加载因子的LinkedHashMap实例。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
*/
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
/**
* 构造一个带指定初始容量和默认加载因子0.75的新空链接哈希set。
*
* 底层会调用父类的构造方法,构造一个带指定初始容量和默认加载因子0.75的LinkedHashMap实例。
* @param initialCapacity 初始容量。
*/
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
/**
* 构造一个带默认初始容量16和加载因子0.75的新空链接哈希set。
*
* 底层会调用父类的构造方法,构造一个带默认初始容量16和加载因子0.75的LinkedHashMap实例。
*/
public LinkedHashSet() {
super(16, .75f, true);
}
/**
* 构造一个与指定collection中的元素相同的新链接哈希set。
*
* 底层会调用父类的构造方法,构造一个足以包含指定collection
* 中所有元素的初始容量和加载因子为0.75的LinkedHashMap实例。
* @param c 其中的元素将存放在此set中的collection。
*/
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
}
由上述源代码可见,LinkedHashSet通过继承HashSet,底层使用LinkedHashMap,以很简单明了的方式来实现了其自身的所有功能。
四、CopyOnWriteArraySet
1、简介
CopyOnWriteArraySet是线程安全的Set集合,相当于线程安全的HashSet。
注意:HashSet的实现是通过散列表HashMap实现的,但是CopyOnWriteArraySet是通过动态数组CopyOnWriteArrayList实现的
2、特性
- 适用于数据量较小且读多写少的场景
- 它是线程安全的
- 新增和删除等修改数据的操作开销很大,涉及到数组复制后续源码解析会详解
- 迭代器只支持读取不支持变更,并且在迭代过程中其他线程写入集合不会发生并发冲突
- 迭代器的数据来源于迭代时的快照数据
- 它最适合于具有以下特征的应用程序:Set 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
- 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等 操作。
- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
3、数据结构
- CopyOnWriteArraySet继承于AbstractSet,这就意味着它是一个集合。
- CopyOnWriteArraySet的底层数据结构是CopyOnWriteArrayList实现的线程安全的可变数组。
- CopyOnWriteArraySet包含CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。
- CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过通过动态数组实现的“集合”。
- CopyOnWriteArrayList中允许有重复的元素;但是,CopyOnWriteArraySet是一个集合,所以它不能有重复集合。
- 因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作!
- 至于CopyOnWriteArraySet的“线程安全”机制,和CopyOnWriteArrayList一样,是通过volatile和互斥锁来实现的。
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
implements java.io.Serializable {
private static final long serialVersionUID = 5457747651344034263L;
private final CopyOnWriteArrayList<E> al;
五、ConcurrentSkipListSet
1、 基本概念
ConcurrentSkipListSet,是J.U.C新增的一个集合工具类,顾名思义,它是一种SET类型。
SET类型,在数学上称为“集合”,具有互异性、无序性的特点,也就是说SET中的任意两个元素均不相同(即不包含重复元素),且元素是无序的。
JDK提供的默认SET实现——HashSet,其实就是采用“组合”的方式——内部引用了一个HashMap对象,以此实现SET的功能。
ConcurrentSkipListSet与ConcurrentSkipListMap的关系,与SET与HashMap的关系类似,就是采用“组合”的方式: ConcurrentSkipListSet组合了一个ConcurrentSkipListMap,将元素作为 ConcurrentSkipListMap的key存放。
2、场景特点
ConcurrentSkipListSet是一种为并发场景设计的有序Set,其特点如下:
-
适用于高并发场景
-
元素是有序的、唯一的
-
内部通过引用一个ConcurrentSkipListMap对象来实现功能
-
添加的值不能为null
2、内部原理
ConcurrentSkipListSet内部通过引用一个ConcurrentSkipListMap对象来实现功能,因为ConcurrentSkipListMap的key就是一个有序的、无重复元素的集合,因此可以满足ConcurrentSkipListSet的功能。
- ConcurrentSkipListSet继承于AbstractSet。因此,它本质上是一个集合。
- ConcurrentSkipListSet实现了NavigableSet接口。因此,ConcurrentSkipListSet是一个有序的集合。
- ConcurrentSkipListSet是通过组合ConcurrentSkipListMap实现的。它包含一个ConcurrentNavigableMap对象m,而m对象实际上是ConcurrentNavigableMap的实现类ConcurrentSkipListMap的实例。ConcurrentSkipListMap中的元素是key-value键值对;而ConcurrentSkipListSet是集合,它只用到了ConcurrentSkipListMap中的key!
3、构造函数
public class ConcurrentSkipListSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {
/**
* The underlying map. Uses Boolean.TRUE as value for each
* element. This field is declared final for the sake of thread
* safety, which entails some ugliness in clone().
*/
private final ConcurrentNavigableMap<E, Object> m;
public ConcurrentSkipListSet() {
m = new ConcurrentSkipListMap<E, Object>();
}
public ConcurrentSkipListSet(Comparator<? super E> comparator) {
m = new ConcurrentSkipListMap<E, Object>(comparator);
}
public ConcurrentSkipListSet(Collection<? extends E> c) {
m = new ConcurrentSkipListMap<E, Object>();
addAll(c);
}
public ConcurrentSkipListSet(SortedSet<E> s) {
m = new ConcurrentSkipListMap<E, Object>(s.comparator());
addAll(s);
}
ConcurrentSkipListSet(ConcurrentNavigableMap<E, Object> m) {
this.m = m;
}
// ...
}
从上述代码可以看出,ConcurrentSkipListSet在构造时创建了一个ConcurrentSkipListMap对象,并由字段m引用,所以其实ConcurrentSkipListSet就是一种跳表类型的数据结构,其平均增删改查的时间复杂度均为O(logn)。