Collection中Set家族:
Set集合的特点
- 无序性:Set集合中的元素没有顺序,即无法通过下标或索引来访问元素。
- 唯一性:Set集合中的元素是唯一的,即不允许重复元素存在。
- 不可变性:Set集合中的元素一旦被添加到集合中,就不能被修改,只能被删除。
- 灵活性:Set集合本身是可变的,即可以添加、删除元素。
- 集合运算:Set集合支持集合运算,如并集、交集、差集等操作。
- 高效性:Set集合的查找、添加、删除等操作都是高效的,时间复杂度为O(1)。
Set集合的实现类
Java中提供了多种Set集合的实现类,如HashSet、TreeSet、LinkedHashSet等,需要了解它们的特点、使用场景和区别。
HashSet特点
- 无序性:HashSet中的元素没有顺序,即不能保证元素的顺序与添加顺序相同。
- 唯一性:HashSet中的元素是唯一的,即不能有重复元素。
- 高效性:HashSet的查找、插入、删除操作都具有很高的效率,时间复杂度为O(1)。
- 底层实现:HashSet的底层实现是基于HashMap实现的,即使用了HashMap的键值对存储方式,但是HashSet只存储键,不存储值。(HashMap是基于哈希表实现的一个Map接口的实现类)
- 线程不安全:HashSet是非线程安全的,如果多个线程同时访问HashSet,可能会导致数据不一致的问题。如果需要在多线程环境下使用HashSet,可以使用Collections.synchronizedSet()方法将HashSet转换为线程安全的Set。
为什么HashSet中的元素没有顺序,也不能重复
HashSet中的元素没有顺序是因为它是基于哈希表实现的,哈希表是一种无序的数据结构。在哈希表中,元素的存储位置是根据它们的哈希值计算得出的,而不是按照它们的插入顺序或者其他顺序来存储的。因此,HashSet中的元素没有顺序。
HashSet中不能重复的原因是因为它是基于哈希表实现的,哈希表中每个元素都有一个唯一的哈希值。当我们向HashSet中添加元素时,它会先计算元素的哈希值,然后根据哈希值来判断该元素是否已经存在于HashSet中。如果已经存在,则不会再次添加,保证了HashSet中元素的唯一性。
什么是哈希表
哈希表是一种数据结构,用于存储和查找数据。
具体来说,哈希表将每个数据元素通过哈希函数计算得到一个哈希值,然后将该值作为数组下标,将数据存储在对应的数组位置上。当需要查找数据时,只需要通过哈希函数计算出该数据的哈希值,然后在数组中查找对应位置上的数据即可。哈希表的优点是查找速度快,但缺点是可能会出现哈希冲突,即不同的数据元素计算得到的哈希值相同,需要使用特定的解决冲突方法来处理。
哈希表类似于字典或者电话簿,它可以通过一个关键字(比如姓名或者电话号码)快速地查找到对应的值(比如地址或者其他联系方式)。在哈希表中,关键字被映射到一个固定的位置,这个位置就是哈希表中的索引
HashSet使用场景
- 去重:HashSet可以用来去除重复的元素,因为它只会保存不重复的元素。
- 查找:HashSet可以用来快速查找某个元素是否存在于集合中,因为它的查找时间复杂度是O(1)。
- 缓存:HashSet可以用来作为缓存,因为它可以快速地判断某个元素是否已经存在于缓存中。
- 集合运算:HashSet可以用来进行集合运算,如求交集(中共同存在)、并集(所有元素组成)、差集(一个集合中去掉另一个集合中的元素所得到的集合)等。
- 数据存储:HashSet可以用来存储数据,因为它可以快速地插入、删除、查找元素。
TreeSet特点
- 有序性:TreeSet中的元素是有序的,它们按照自然顺序或者指定的比较器顺序进行排序。
- 唯一性:TreeSet中的元素是唯一的,它们不会重复。
- 可排序性:TreeSet中的元素可以按照自然顺序或者指定的比较器顺序进行排序。
- 底层数据结构:TreeSet底层采用红黑树实现,因此插入、删除、查找等操作的时间复杂度为O(log n)。
- 线程不安全:TreeSet不是线程安全的,如果多个线程同时访问一个TreeSet对象,可能会出现并发问题。
- 不允许空元素:TreeSet不允许插入空元素,否则会抛出NullPointerException异常。
为什么TreeSet中的元素是有序的,并且不能重复
因为它是基于红黑树实现的。红黑树是一种自平衡二叉查找树,它能够保证在插入、删除元素时,树的高度始终保持在log(n)级别,从而保证了查找、插入、删除等操作的时间复杂度都是O(logn)
O(logn) 是一种时间复杂度的表示方法,表示算法的运行时间与输入规模 n 的对数成正比。也就是说,当输入规模 n 增加时,算法的运行时间不会呈线性增长,而是以对数的方式增长。
举个例子,假设有一个有序数组,我们想要在其中查找一个元素。如果我们使用线性查找,需要遍历整个数组,时间复杂度为 O(n)。但是如果我们使用二分查找,每次可以将查找范围缩小一半,时间复杂度就是 O(logn)
什么是红黑树
特点
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色的。
- 每个叶子节点(NIL节点,空节点)是黑色的。
- 如果一个节点是红色的,则它的两个子节点都是黑色的。
- 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
缺点
- 实现复杂:红黑树的实现比较复杂,需要考虑多种情况,包括旋转、颜色变换等操作,容易出错。
- 空间占用较大:红黑树需要维护额外的颜色信息,因此相比于其他平衡树,它的空间占用较大。
- 不适合频繁插入删除操作:虽然红黑树的插入和删除操作的时间复杂度都是O(log n),但是由于需要维护平衡,每次插入和删除操作都需要进行旋转和颜色变换,因此相比于其他数据结构,它不太适合频繁进行插入和删除操作。
- 不支持高效的区间查询:红黑树虽然支持快速的查找、插入和删除操作,但是不支持高效的区间查询操作,需要遍历整棵树来查找符合条件的节点,时间复杂度为O(n)。
TreeSet使用场景
- 需要对元素进行排序的场景:TreeSet内部采用红黑树实现,可以自动对元素进行排序,因此适用于需要对元素进行排序的场景。
- 需要去重的场景:TreeSet内部采用红黑树实现,可以自动去重,因此适用于需要去重的场景。
- 需要快速查找元素的场景:TreeSet内部采用红黑树实现,可以快速查找元素,因此适用于需要快速查找元素的场景。
- 需要按照范围查找元素的场景:TreeSet提供了一系列的方法,可以按照范围查找元素,例如:headSet、tailSet、subSet等方法,因此适用于需要按照范围查找元素的场景。
- 需要对元素进行高级操作的场景:TreeSet提供了一系列的方法,可以对元素进行高级操作,例如:first、last、ceiling、floor等方法,因此适用于需要对元素进行高级操作的场景。
LinkedHashSet特点
- 有序性:LinkedHashSet中的元素是按照插入顺序进行排序的,因此可以保证元素的顺序性。
- 可以避免重复元素:LinkedHashSet中不允许存储重复元素,如果尝试添加重复元素,将会被忽略。
- 性能较好:LinkedHashSet的性能与HashSet相当,但是由于需要维护元素的插入顺序,因此在插入和删除元素时可能会稍微慢一些。
- 底层实现是HashMap:LinkedHashSet的底层实现是一个HashMap,因此具有HashMap的所有特点,如快速查找、高效存储等。
为什么LinkedHashSet是按照顺序进行排序的
LinkedHashSet的底层实现是一个哈希表和一个双向链表。哈希表用于快速查找元素,双向链表用于维护元素的插入顺序。当元素被插入时,它会被添加到哈希表中,并且在链表的末尾添加一个新节点。当元素被删除时,它会从哈希表中删除,并且从链表中删除相应的节点。
LinkedHashSet的应用场景包括:
- 需要维护元素的插入顺序。
- 需要快速查找、删除和插入元素。
- 不允许元素重复。
- 需要保证元素的顺序不变。
HashSet源码解析
JDK1.8,哈希表采用的数据结构:
哈希表采用的数据结构是数组+链表/红黑树的组合结构。具体来说,哈希表中的每个元素都是一个链表或红黑树,数组中的每个元素指向一个链表或红黑树的根节点。当哈希表中的元素数量较少时,每个元素都是一个链表;当元素数量较多时,会将链表转化为红黑树,以提高查找效率
JDK1.8之前,哈希表采用的数据结构:
哈希表采用的数据结构是数组和链表的组合,也就是链表散列。每个数组元素都是一个链表的头节点,当发生哈希冲突时,新的元素会被插入到对应数组元素的链表中。这种数据结构的缺点是在哈希冲突严重时,链表会变得很长,导致查询效率降低。
数组和链表+红黑树为什么比数组+链表更高效
- 数组:随机访问元素的时间复杂度为O(1),插入和删除元素的时间复杂度为O(n)
- 链表:插入和删除元素的时间复杂度为O(1),随机访问元素的时间复杂度为O(n)
- 红黑树的时间复杂度为O(log n),具有较好的平衡性和搜索性能,适用于需要频繁插入、删除和搜索的场景。 因此,红黑树相比于数组和链表,具有更高的效率,尤其是在需要频繁插入、删除和搜索的场景下。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
该类继承了AbstractSet类,实现了Set接口,并且支持克隆和序列化。泛型参数E表示集合中元素的类型,提供了一些方法来操作集合,例如添加、删除、包含、遍历等。
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
序列化版本号和一个私有的 HashMap 对象,使用了一个静态的 final Object 对象作为 value,用于关联 HashMap 中的 key。
transient 关键字用于修饰类的成员变量,,表示该变量不会被序列化。在反序列化对象时,transient 修饰的变量会被赋予默认值,例如数字类型的变量会被赋值为 0,对象类型的变量会被赋值为 null
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
第一个构造方法是无参构造方法,直接创建一个空的 HashMap 对象。第二个构造方法是带有一个集合参数的构造方法,先根据集合的大小计算出 HashMap 的初始容量,然后将集合中的元素添加到 HashMap 中。
如果集合 c 的大小除以 0.75 后得到的值大于 16,则使用这个值作为 HashSet 的初始容量;否则,使用 16 作为初始容量
//接收一个初始容量和负载因子作为参数,
//它会创建一个指定初始容量和负载因子的 HashMap 对象作为 HashSet 的底层实现
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
//接收一个初始容量HashMap 对象作为 HashSet 的底层实现
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//这个构造方法创建一个具有指定初始容量和负载因子的 HashSet,
//并使用 LinkedHashMap 作为其底层实现。这个构造方法通常不会被直接使用,而是由 HashSet 的子类调用
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
//返回一个迭代器,用于遍历Map中所有的key
public Iterator<E> iterator() {
return map.keySet().iterator();
}
//返回此set中的元素的数
public int size() {
return map.size();
}
//判断map是否为空,返回此set中的元素的数
public boolean isEmpty() {
return map.isEmpty();
}
//底层实际调用HashMap的containsKey判断是否包含指定key,如果此set包含指定元素,则返回true。
public boolean contains(Object o) {
return map.containsKey(o);
}
/* 底层实际将将该元素作为key放入HashMap。
* 由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key
* 与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true),
* 新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变,
* 因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中,
* 原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。
/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
/**
* 如果指定元素存在于此set中,则将其移除。
* 如果此set已包含该元素,则返回true
* * 底层实际调用HashMap的remove方法删除指定Entry。
/
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
/**
* 从此set中移除所有元素。此调用返回后,该set将为空。
* 底层实际调用HashMap的clear方法清空Entry中所有元素。
*/
public void clear() {
map.clear();
}
//首先,它调用父类的Cloneable接口的clone()方法方法来创建一个新的HashSet对象
//然后,它将原始HashSet对象的map字段的副本分配给新的HashSet对象的map字段
//以确保两个对象具有相同的元素。最后,它返回新的HashSet对象。
@SuppressWarnings("unchecked") //抑制编译器产生的“未经检查的转换”警告信息,常用于泛型
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);
}
}
//用于将HashSet对象序列化为字节流并写入输出流中
//s.defaultWriteObject():调用默认的序列化方法,将HashSet对象的非transient字段写入输出流中。
//s.writeInt(map.capacity()):将HashSet对象内部使用的HashMap的容量写入输出流中。
//s.writeFloat(map.loadFactor()):将HashSet对象内部使用的HashMap的负载因子写入输出流中。
// s.writeInt(map.size()):将HashSet对象内部使用的HashMap中元素的数量写入输出流中。
//遍历HashSet对象内部使用的HashMap中的所有键,并将它们写入输出流中。注意,这里只写入键而不写入值
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
s.writeInt(map.size());
for (E e : map.keySet())
s.writeObject(e);
}
//该方法是用于反序列化HashSet对象的私有方法,它会从输入流中读取HashSet对象的各个属性,并根据这些属性重新构建一个新的HashSet对象。在读取过程中,如果发现某些属性的值不合法,则会抛出InvalidObjectException异常
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
SharedSecrets.getJavaOISAccess()
.checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
//该方法返回一个Spliterator对象,用于遍历HashMap中所有的键值。Spliterator是Java 8中新增的接口,用于支持并行遍历和分割迭代器。
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
}