一、Set介绍
Set是Java中的一个接口,它继承自Collection接口,用于表示不允许重复元素的集合。它定义了操作集合元素的方法,如添加、删除、查找等。
Set接口的实现类有以下几种:
-
HashSet:底层实现是基于哈希表。它是一种最快的查找算法,但是不保证元素的顺序。
-
LinkedHashSet:底层实现是基于哈希表和双向链表。它保留了元素插入的顺序,但是比HashSet慢一些。
-
TreeSet:底层实现是基于红黑树。它是一种有序的集合,元素按照从小到大的顺序排列。但是插入和删除操作稍微慢一些。
Set集合的特点:
-
不允许重复元素,如果尝试添加重复元素会被忽略;
-
无序性,不保证元素的顺序,HashSet、LinkedHashSet没有排序;TreeSet是按照元素的大小排序。
-
Set接口中的方法只能是Collection接口中定义的方法,没有自己的新方法。主要使用Collection接口中的add(E e), remove(Object o), contains(Object o), size()方法。
Set为什么是无序的并且不可重复?
Set是Java中的一种集合,它只存储不重复的元素。这是因为Set的设计初衷是为了去重。Set中元素的唯一性是通过元素的hashCode()值和equals()方法来判断的。如果Set中允许存储重复元素,就会造成判断唯一性时的混淆,从而失去了去重的功能。
同时,Set是一种无序的集合,它不保证元素存储的顺序和添加的顺序相同。这是因为Set内部使用了哈希算法对元素进行存储和查找,为了保证存储效率,Set不会保留元素的顺序。但是,也可以通过转换为List再排序等手段来实现按顺序存储。
以下是一个简单的示例代码:
import java.util.HashSet;
import java.util.Set;
public class SetDemo {
public static void main(String[] args) {
// 创建一个HashSet对象,存储String类型的元素
Set<String> set = new HashSet<>();
// 向set中添加元素
set.add("hello");
set.add("world");
set.add("java");
set.add("hello"); // 重复元素,不会被添加进去
// 打印set中的所有元素
System.out.println(set);
}
}
在上述代码中,我们创建了一个HashSet集合,并向其中添加了四个元素。由于Set不允许重复元素,"hello"只会被添加一次。最终输出的结果为:
可以看到,输出结果是无序的。这是因为Set内部使用了哈希算法进行存储和查找,而哈希算法会将元素散列到不同的位置,因此不保留元素的顺序。
需要注意的是,如果我们使用TreeSet来实现Set,它会按照元素的自然顺序进行排序。但是如果元素类型没有实现Comparable接口,则会抛出ClassCastException异常。因此在使用TreeSet时,需要注意元素的类型是否支持排序。
二、HashSet实现Set
HashSet的底层实现:HashSet采用哈希表实现,哈希表是一种能够快速定位元素的数据结构。HashSet将元素的hashCode()值作为哈希值,将元素存储在对应的哈希表中。哈希表的每一个位置存储一个链表,哈希值相同的元素会被存储在同一个链表中。
代码实现:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{
// 序列号
static final long serialVersionUID = -5024744406713321676L;
// 存放元素的哈希表,transient修饰表示该变量不会被序列化
private transient HashMap<E,Object> map;
// 原值,为一个Object对象
private static final Object PRESENT = new Object();
// HashSet构造函数,指定初始容量和负载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<E,Object>(initialCapacity, loadFactor);
}
// HashSet构造函数,指定初始容量
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity);
}
// HashSet构造函数,默认初始容量为16,负载因子为0.75f
public HashSet() {
map = new HashMap<E,Object>();
}
// HashSet构造函数,传入一个集合
public HashSet(Collection<? extends E> c) {
map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1,16));
addAll(c);
}
// 返回元素个数
public int size() {
return map.size();
}
// 判断是否为空
public boolean isEmpty() {
return map.isEmpty();
}
// 判断是否包含元素o
public boolean contains(Object o) {
return map.containsKey(o);
}
// 添加元素e
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// 删除元素o
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
// 清空集合
public void clear() {
map.clear();
}
// 返回集合的迭代器
public Iterator<E> iterator() {
return map.keySet().iterator();
}
// 返回集合的副本
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E,Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
// 将集合转换为数组
public Object[] toArray() {
return map.keySet().toArray();
}
// 将集合转换为指定类型的数组
public <T> T[] toArray(T[] a) {
return map.keySet().toArray(a);
}
// 重写Object类的equals()方法
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Set<?> set = (Set<?>) o;
try {
return containsAll(set) && set.containsAll(this);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
// 返回哈希值
public int hashCode() {
int h = 0;
Iterator<E> i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
}
return h;
}
// 私有方法,读取对象输入流
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException
{
stream.defaultReadObject();
int capacity = stream.readInt();
float loadFactor = stream.readFloat();
map = (((HashSetReflectionFactory) ReflectionFactory.getReflectionFactory()).
newHashMap(capacity, loadFactor));
int size = stream.readInt();
for (int i=0; i<size; i++) {
E e = (E) stream.readObject();
add(e);
}
}
}
说明:
- 通过一个继承于HashMap的类,实现了Set接口。
- HashSet使用哈希表来存储元素。
- 每个位置存储一个链表,哈希值相同的元素会被存储在同一个链表中。
- HashSet的add()方法调用的是HashMap的put()方法,将元素作为key,Object类型的静态常量PRESENT作为value,将其放入哈希表。
- HashSet的remove()方法调用的是HashMap的remove()方法,返回值为PRESENT时表示该元素存在于集合中。
- HashSet转换为数组时,调用的是HashMap的keySet()方法,返回的是一个Set类型的元素集合,再将集合转换为数组。
- HashSet使用Object类的equals()方法判断两个集合是否相等,判断两个集合是否包含相同的元素。使用元素的hashCode()值作为哈希值,将元素存储在对应的哈希表中。
三、LinkedHashSet实现Set
LinkedHashSet的底层实现:LinkedHashSet在HashSet的基础上增加了链表维护元素的插入顺序,元素插入链表尾部。
代码实现:
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable{
// 序列号
private static final long serialVersionUID = -2851667679971038690L;
// 存放元素的哈希表
private final LinkedHashMap<E,Object> map;
// 传入负载因子、是否按照访问顺序排序
public LinkedHashSet(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor, accessOrder);
}
// 传入负载因子
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}
// 使用默认的负载因子
public LinkedHashSet(int initialCapacity) {
super(initialCapacity);
map = new LinkedHashMap<E,Object>(initialCapacity);
}
// 使用默认的初始容量和负载因子
public LinkedHashSet() {
super();
map = new LinkedHashMap<E,Object>();
}
// 接收集合类型参数
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11));
map = new LinkedHashMap<E,Object>(Math.max(2*c.size(), 11));
addAll(c);
}
// 返回元素个数
public int size() {
return map.size();
}
// 判断是否为空
public boolean isEmpty() {
return map.isEmpty();
}
// 判断是否包含元素o
public boolean contains(Object o) {
return map.containsKey(o);
}
// 添加元素e
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// 删除元素o
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
// 清空集合
public void clear() {
map.clear();
}
// 返回集合的迭代器
public Iterator<E> iterator() {
return map.keySet().iterator();
}
// 返回集合的副本
public Object clone() {
try {
LinkedHashSet<E> newSet = (LinkedHashSet<E>) super.clone();
newSet.map = (LinkedHashMap<E,Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
}
说明:
- LinkedHashSet继承自HashSet,在HashSet的基础上增加了链表来维护元素的插入顺序。
- 存放元素的哈希表使用LinkedHashMap实现,链表按照插入顺序排序。
- LinkedHashSet的iterator()方法返回的是一个迭代器,迭代顺序与元素插入顺序一致。
- LinkedHashSet继承了HashSet的clone()方法,返回一个副本,其中map也会被复制一份。
四、TreeSet实现Set
TreeSet的底层实现:TreeSet是一种基于红黑树的Set集合,元素按照自然排序或自定义比较器排序。
代码实现:
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable{
// 序列号
private static final long serialVersionUID = -2479143000061671589L;
// 存放元素的红黑树
private transient NavigableMap<E,Object> m;
// 最低元素
public Object first() {
return m.firstKey();
}
// 最高元素
public Object last() {
return m.lastKey();
}
// 小于e的最大元素
public Object lower(Object e) {
return m.lowerKey(e);
}
// 小于等于e的最大元素
public Object floor(Object e) {
return m.floorKey(e);
}
// 大于e的最小元素
public Object ceiling(Object e) {
return m.ceilingKey(e);
}
// 大于等于e的最小元素
public Object higher(Object e) {
return m.higherKey(e);
}
// 移除第一个元素,并返回它
public Object pollFirst() {
Map.Entry e = m.pollFirstEntry();
return e == null ? null : e.getKey();
}
// 移除最后一个元素,并返回它
public Object pollLast() {
Map.Entry e = m.pollLastEntry();
return e == null ? null : e.getKey();
}
// 尾部部分视图
public NavigableSet descendingSet() {
return new TreeSet(m.descendingMap());
}
// 不作修改返回集合
public Iterator iterator() {
return m.keySet().iterator();
}
// 不作修改返回集合的大小
public int size() {
return m.size();
}
// 返回是否为空
public boolean isEmpty() {
return m.isEmpty();
}
// 返回是否包含o
public boolean contains(Object o) {
return m.containsKey(o);
}
// 添加e
public boolean add(Object e) {
return m.put(e, PRESENT) == null;
}
// 移除o
public boolean remove(Object o) {
return m.remove(o) == PRESENT;
}
// 清空集合
public void clear() {
m.clear();
}
// 返回带比较器的迭代器
public Comparator comparator() {
return m.comparator();
}
// 从NavigableSet接口继承的方法
public NavigableSet subSet(Object fromElement, boolean fromInclusive, Object toElement, boolean toInclusive) {
return new TreeSet(m.subMap(fromElement, fromInclusive, toElement, toInclusive));
}
public NavigableSet headSet(Object toElement, boolean inclusive) {
return new TreeSet(m.headMap(toElement, inclusive));
}
public NavigableSet tailSet(Object fromElement, boolean inclusive) {
return new TreeSet(m.tailMap(fromElement, inclusive));
}
public SortedSet subSet(Object fromElement, Object toElement) {
return subSet(fromElement, true, toElement, false);
}
public SortedSet headSet(Object toElement) {
return headSet(toElement, false);
}
public SortedSet tailSet(Object fromElement) {
return tailSet(fromElement, true);
}
// 克隆集合
public Object clone() {
TreeSet clone;
try {
clone = (TreeSet) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
clone.m = new TreeMap(m);
return clone;
}
// PRESENT对象用于put方法中value的占位符
private static final Object PRESENT = new Object();
}
五、多线程下的Set集合的安全性
不安全的HashSet
Set集合在多线程下不安全。因为多线程并发操作时,可能会出现同时对同一个Set进行修改的情况,导致数据出现错误或重复。例如,一个线程正在向Set中添加元素,另一个线程正在从Set中删除元素,这样就可能出现一些意想不到的情况。
以下是一个多线程下对Set进行操作的示例代码:
import java.util.HashSet;
import java.util.Set;
public class SetDemo {
//定义一个全局的初始化Set
private static Set<Integer>set =new HashSet<>();
public static void main(String[] args) throws InterruptedException {
//定义两个线程
Thread t1 = new Thread(new AddTask());
Thread t2 = new Thread(new RemoveTask());
t1.start();
t2.start();
t1.join();
t2.join();
//输出最终Set的大小
System.out.println("Set size: " + set.size());
}
/**
* 添加数据到Set集合
*/
static class AddTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
set.add(i);
}
}
}
/**
* 将Set集合中的数据删除
*/
static class RemoveTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
set.remove(i);
}
}
}
}
在上述示例代码中,两个线程分别执行添加和删除操作,如果Set是线程安全的,程序输出的Set大小应该为0。但是,由于Set在多线程下不安全,因此可能会出现一些错误的输出结果。
为了保证Set在多线程下的安全,可以使用线程安全的Set实现,比如ConcurrentSkipListSet
和CopyOnWriteArraySet
。这些实现都是线程安全的,可以保证多线程并发操作时不出现数据错误或重复。
安全的 ConcurrentSkipListSet 和 CopyOnWriteArraySet
ConcurrentSkipListSet
和CopyOnWriteArraySet
在多线程下是线程安全的,主要原因是它们都采用了不同的实现方式,可以保证多线程并发访问时不会出现数据错误或重复。
ConcurrentSkipListSet
是基于Skip List的数据结构实现的,并采用了一些特殊的算法来保证数据的线程安全性。具体来说,ConcurrentSkipListSet
中的节点是按照Key的大小顺序进行排序的,各个节点之间通过链表相连。在对节点进行操作时,ConcurrentSkipListSet
会先锁定节点所在的层数,再通过CAS(Compare and Swap)操作来更新节点信息,从而保证数据的线程安全性和可靠性。
CopyOnWriteArraySet
则是通过在进行写操作时复制整个数组来实现线程安全性的。具体来说,CopyOnWriteArraySet
中的内部数组是不可变的,而添加、删除等操作则会创建一个新的数组,然后将原数组中的元素复制到新数组中,并在新数组中进行操作。由于对旧数组的修改不会影响到新数组,因此CopyOnWriteArraySet
可以避免多线程并发修改的问题,从而保证数据的线程安全性和可靠性。
以下是两种实现方式的示例代码:
ConcurrentSkipListSet示例:
import java.util.concurrent.ConcurrentSkipListSet;
public class ConcurrentSkipListSetTest{
private static ConcurrentSkipListSet<Integer> set = new ConcurrentSkipListSet<>();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AddTask());
Thread t2 = new Thread(new RemoveTask());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("ConcurrentSkipListSetTest size: " + set.size());
}
static class AddTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
set.add(i);
}
}
}
static class RemoveTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (set.contains(i)) {
set.remove(i);
}
}
}
}
}
上述示例代码中,两个线程分别对ConcurrentSkipListSet进行添加和删除操作。由于ConcurrentSkipListSet是线程安全的,因此不会出现数据重复或错误的情况。
CopyOnWriteArraySet示例:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArraySet;
public class CopyOnWriteArraySetTest {
private static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AddTask());
Thread t2 = new Thread(new RemoveTask());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("CopyOnWriteArraySetTest size: " + set.size());
}
static class AddTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
set.add(i);
}
}
}
static class RemoveTask implements Runnable {
@Override
public void run() {
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
int value = iterator.next();
if (value % 2 == 0) {
iterator.remove();
}
}
}
}
}
上述示例代码中,两个线程分别对CopyOnWriteArraySet进行添加和删除操作。由于CopyOnWriteArraySet是线程安全的,因此不会出现数据重复或错误的情况。
需要注意的是,在使用CopyOnWriteArraySet
时,由于每次添加、删除等操作都会创建一个新的数组,因此可能会影响程序的性能和内存占用。因此,应该根据实际情况选择合适的集合实现方式。
另外需要注意的是,虽然ConcurrentSkipListSet
和CopyOnWriteArraySet
都是线程安全的集合,但它们的实现方式不同,因此在不同的场景下应选择合适的实现方式。一般来说,如果需要在高并发场景下进行大量的读取操作,可以选择ConcurrentSkipListSet
;如果需要在写操作较多的情况下保证数据的线程安全性,可以选择CopyOnWriteArraySet
。
总之,在多线程开发中,选择合适的集合实现方式是保证程序正确性和性能的重要因素之一。
ConcurrentSkipListSet 和 ConcurrentSkipListSet 代码讲解
1、ConcurrentSkipListSet
ConcurrentSkipListSet示例代码中,我们首先定义了一个ConcurrentSkipListSet
类型的成员变量set
,然后创建了两个线程t1
和t2
,分别执行添加操作和删除操作。
在添加操作AddTask
中,我们使用了for
循环来向set
集合中添加10000个元素,这些添加操作可能会发生并发。
在删除操作RemoveTask
中,我们同样使用了for
循环和remove
方法来从set
集合中删除元素。需要注意的是,我们在删除前使用了contains
方法来判断该元素是否存在,这是为了避免删除不存在的元素时出现异常。这些删除操作同样可能会发生并发。
最后,我们在主线程中使用join
方法等待两个线程执行完成,然后输出最终的set
集合大小。
由于ConcurrentSkipListSet
是线程安全的集合,因此上述示例代码可以在多线程并发操作的情况下保证数据的正确性和可靠性。实际上,ConcurrentSkipListSet
的线程安全性是依靠内部的读写锁和CAS操作实现的。每当一个线程在对集合进行添加或删除操作时,它会锁住指定的节点,然后更新节点信息,最后解锁该节点。这样可以避免多个线程同时对同一个节点进行操作,从而保证线程安全性。
2、CopyOnWriteArraySet
CopyOnWriteArraySet示例代码中,我们同样首先定义了一个CopyOnWriteArraySet
类型的成员变量set
,然后创建了两个线程t1
和t2
,分别执行添加操作和删除操作。
在添加操作AddTask
中,我们同样使用了for
循环来向set
集合中添加100个元素。
在删除操作RemoveTask
中,我们同样使用了for
循环和iterator
迭代器来遍历set
集合,并使用remove
方法来删除符合条件的元素。需要注意的是,由于CopyOnWriteArraySet
的特性,每次删除操作都会创建一个新的数组并将原数组中的元素复制到新数组中,因此删除操作的效率较低。
最后,我们同样在主线程中使用join
方法等待两个线程执行完成,然后输出最终的set
集合大小。
由于CopyOnWriteArraySet
是线程安全的集合,因此上述示例代码同样可以在多线程并发操作的情况下保证数据的正确性和可靠性。实际上,CopyOnWriteArraySet
的线程安全性是依靠内部数组的不可变性和写操作的原子性来实现的。每当一个线程在对集合进行添加或删除操作时,它会创建一个新的数组,并复制原数组中的元素到新数组中,然后进行操作。这样可以避免多个线程同时对同一个原数组进行写操作,从而保证线程安全性。
但是,由于CopyOnWriteArraySet
每次写操作都会创建新数组并进行复制操作,因此其性能不如ConcurrentSkipListSet
。在多线程并发操作的场景下,我们应该根据实际情况选择合适的线程安全集合来优化程序性能和可靠性。