0 Set 接口

Set接口

Set接口继承了Collection接口,是一个不包括重复元素的集合,更确切地说,Set 中任意两个元素不会出现 o1.equals(o2),而且 Set 至多只能存储一个 NULL 值元素,Set 集合的组成部分可以用下面这张图概括:

1595682050240-6c6946f2-9dd4-4e5b-a006-39144184e2f1.png

在 Set 集合体系中,我们需要着重关注两点:

  • 存入可变元素时,必须非常小心,因为任意时候元素状态的改变都有可能使得 Set 内部出现两个相等的元素,即 o1.equals(o2) = true,所以一般不要更改存入 Set 中的元素,否则将会破坏了 equals() 的作用!

  • Set 的最大作用就是判重,在项目中最大的作用也是判重

接下来我们去看它的实现类和子类: AbstractSetSortedSet

AbstractSet 抽象类

AbstractSet 是一个实现 Set 的一个抽象类,定义在这里可以将所有具体 Set 集合的相同行为在这里实现,避免子类包含大量的重复代码

所有的 Set 也应该要有相同的 hashCode()equals() 方法,所以使用抽象类把该方法重写后,子类无需关心这两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class AbstractSet<E> implements Set<E> {
// 判断两个 set 是否相等
public boolean equals(Object o) {
if (o == this) { // 集合本身
return true;
} else if (!(o instanceof Set)) { // 集合不是 set
return false;
} else {
// 比较两个集合的元素是否全部相同
}
}
// 计算所有元素的 hashcode 总和
public int hashCode() {
int h = 0;
Iterator i = this.iterator();
while(i.hasNext()) {
E obj = i.next();
if (obj != null) {
h += obj.hashCode();
}
}
return h;
}
}

SortedSet 接口

SortedSet 是一个接口,它在 Set 的基础上扩展了排序的行为,所以所有实现它的子类都会拥有排序功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface SortedSet<E> extends Set<E> {
// 元素的比较器,决定元素的排列顺序
Comparator<? super E> comparator();
// 获取 [var1, var2] 之间的 set
SortedSet<E> subSet(E var1, E var2);
// 获取以 var1 开头的 Set
SortedSet<E> headSet(E var1);
// 获取以 var1 结尾的 Set
SortedSet<E> tailSet(E var1);
// 获取首个元素
E first();
// 获取最后一个元素
E last();
}

1 HashSet

底层实现

HashSet 底层借助 HashMap 实现,我们可以观察它的多个构造方法,本质上都是 new 一个 HashMap

1
2
3
4
5
6
7
8
9
10
11
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
public HashSet() {
this.map = new HashMap();
}
public HashSet(int initialCapacity, float loadFactor) {
this.map = new HashMap(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
this.map = new HashMap(initialCapacity);
}
}

我们可以观察 add() 方法和remove()方法是如何将 HashSet 的操作嫁接到 HashMap 的。

1
2
3
4
5
6
7
8
private static final Object PRESENT = new Object();

public boolean add(E e) {
return this.map.put(e, PRESENT) == null;
}
public boolean remove(Object o) {
return this.map.remove(o) == PRESENT;
}

我们看到 PRESENT 就是一个静态常量:使用 PRESENT 作为 HashMap 的 value 值,使用HashSet的开发者只需关注于需要插入的 key屏蔽了 HashMap 的 value

上图可以观察到每个Entryvalue都是 PRESENT 空对象,我们就不用再理会它了。

HashSet 在 HashMap 基础上实现,所以很多地方可以联系到 HashMap:

  • 底层数据结构:HashSet 也是采用数组 + 链表 + 红黑树实现
  • 线程安全性:由于采用 HashMap 实现,而 HashMap 本身线程不安全,在HashSet 中没有添加额外的同步策略,所以 HashSet 也线程不安全
  • 存入 HashSet 的对象的状态最好不要发生变化,因为有可能改变状态后,在集合内部出现两个元素o1.equals(o2),破坏了 equals()的语义。

继承结构

  • 实现了Set接口。
  • HashSet中不允许重复的值。
  • HashSet中允许一个NULL元素。
  • 无序集合,并且不保证集合的迭代顺序。
  • 为基本操作(添加,删除,包含和调整大小)提供恒定的时间性能。
  • HashSet不同步。 如果多个线程同时访问哈希集,并且至少有一个线程修改了哈希集,则必须在外部对其进行同步。使用Collections.synchronizedSet(new HashSet())方法获取同步的哈希集。

基本使用

  • 默认初始容量为16 。 我们可以通过在构造函数HashSet(int initialCapacity)传递默认容量来覆盖此默认容量。
1
2
3
4
5
6
7
8
9
10
public boolean add(E e) :如果指定的元素不存在,则将其添加到Set中。 此方法在内部使用equals()方法检查重复项。 如果元素重复,则元素被拒绝,并且不替换值。
public void clear() :从哈希集中删除所有元素。
public boolean contains(Object o) :如果哈希集包含指定的元素othrweise false ,则返回false
public boolean isEmpty() :如果哈希集不包含任何元素,则返回true ,否则返回false
public int size() :返回哈希集中的元素数量。
public Iterator<E> iterator() :返回对此哈希集中的元素的迭代器。 从迭代器返回的元素没有特定的顺序。
public boolean remove(Object o) :从哈希集中删除指定的元素(如果存在)并返回true ,否则返回false
public boolean removeAll(Collection <?> c) :删除哈希集中属于指定集合的​​所有元素。
public Object clone() :返回哈希集的浅表副本。
public Spliterator<E> spliterator() :在此哈希集中的元素上创建后绑定和故障快速的Spliterator。

2 LinkedHashSet

底层原理

LinkedHashSet 的代码少的可怜,不信我给你我粘出来

少归少,还是不能闹,LinkedHashSet继承了HashSet,我们跟随到父类 HashSet 的构造方法看看

1
2
3
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
this.map = new LinkedHashMap(initialCapacity, loadFactor);
}

发现父类中 map 的实现采用LinkedHashMap,这里注意不是HashMap,而 LinkedHashMap 底层又采用 HashMap + 双向链表 实现的,所以本质上 LinkedHashSet 还是使用 HashMap 实现的。

LinkedHashSet -> LinkedHashMap -> HashMap + 双向链表

image.png

而 LinkedHashMap 是采用 HashMap双向链表实现的,这条双向链表中保存了元素的插入顺序。所以 LinkedHashSet 可以按照元素的插入顺序遍历元素,如果你熟悉LinkedHashMap,那 LinkedHashSet 也就更不在话下了。

关于 LinkedHashSet 需要注意几个地方:

  • 它继承了 HashSet,而 HashSet 默认是采用 HashMap 存储数据的,但是 LinkedHashSet 调用父类构造方法初始化 map 时是 LinkedHashMap 而不是 HashMap,这个要额外注意一下
  • 由于 LinkedHashMap 不是线程安全的,且在 LinkedHashSet 中没有添加额外的同步策略,所以 LinkedHashSet 集合也不是线程安全

继承关系

  • 它扩展了HashSet类,后者扩展了AbstractSet类。
  • 它实现Set接口。
  • LinkedHashSet中不允许重复的值 。
  • LinkedHashSet中允许一个NULL元素。
  • 它是一个ordered collection ,它是元素插入到集合中insertion-order ( insertion-order )。
  • 像HashSet一样,此类为基本操作(添加,删除,包含和调整大小)提供constant time performance 。
  • LinkedHashSet not synchronized 。 如果多个线程同时访问哈希集,并且至少有一个线程修改了哈希集,则必须在外部对其进行同步。
  • 使用Collections.synchronizedSet(new LinkedHashSet())方法来获取同步的LinkedHashSet。

使用方法

1
2
3
4
5
6
7
8
9
10
public boolean add(E e) :如果指定的元素不存在,则将其添加到Set中。 此方法在内部使用equals()方法检查重复项。 如果元素重复,则元素被拒绝,并且不替换值。
public void clear() :从LinkedHashSet中删除所有元素。
public boolean contains(Object o) :如果LinkedHashSet包含指定的元素othrweise false ,则返回false
public boolean isEmpty() :如果LinkedHashSet不包含任何元素,则返回true ,否则返回false
public int size() :返回LinkedHashSet中的元素数。
public Iterator<E> iterator() :返回对此LinkedHashSet中的元素的迭代器。 从迭代器返回的元素没有特定的顺序。
public boolean remove(Object o) :从LinkedHashSet中移除指定的元素(如果存在)并返回true ,否则返回false
public boolean removeAll(Collection <?> c) :删除LinkedHashSet中属于指定集合的​​所有元素。
public Object clone() :返回LinkedHashSet的浅表副本。
public Spliterator<E> spliterator() :在此LinkedHashSet中的元素上创建后绑定和故障快速的Spliterator。 它具有以下初始化属性Spliterator.DISTINCT , Spliterator.ORDERED 。

3 TreeSet

底层原理

TreeSet 是基于 TreeMap 的实现,所以存储的元素是有序的,底层的数据结构是数组 + 红黑树

img

而元素的排列顺序有2种,和 TreeMap 相同:自然排序和定制排序,常用的构造方法已经在下面展示出来了,TreeSet 默认按照自然排序,如果需要定制排序,需要传入Comparator

1
2
3
4
5
6
public TreeSet() { 
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}

TreeSet 应用场景有很多,像在游戏里的玩家战斗力排行榜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Player implements Comparable<Integer> {
public String name;
public int score;
@Override
public int compareTo(Student o) {
return Integer.compareTo(this.score, o.score);
}
}
public static void main(String[] args) {
Player s1 = new Player("张三", 100);
Player s2 = new Player("李四", 90);
Player s3 = new Player("王五", 80);
TreeSet<Player> set = new TreeSet();
set.add(s2); set.add(s1); set.add(s3);
System.out.println(set);
}
// [Student{name='王五', score=80}, Student{name='李四', score=90}, Student{name='张三', score=100}]

对 TreeSet 介绍了它的主要实现方式和应用场景,有几个值得注意的点。

  • TreeSet 的所有操作都会转换为对 TreeMap 的操作,TreeMap 采用红黑树实现,任意操作的平均时间复杂度为 O(logN)
  • TreeSet 是一个线程不安全的集合
  • TreeSet 常应用于对不重复的元素定制排序,例如玩家战力排行榜

注意:TreeSet判断元素是否重复的方法是判断compareTo()方法是否返回0,而不是调用 hashcode() 和 equals() 方法,如果返回 0 则认为集合内已经存在相同的元素,不会再加入到集合当中。

继承关系

  • 它扩展了AbstractSet类,该类扩展了AbstractCollection类。
  • 它实现了NavigableSet接口,该接口扩展了SortedSet接口。
  • TreeSet中不允许重复的值。
  • 在TreeSet中不允许NULL。
  • 它是一个ordered collection ,按排序顺序存储元素。
  • 与HashSet一样,此类为基本操作(添加,删除,包含和调整大小)提供恒定的时间性能。
  • TreeSet不允许插入异构对象,因为它必须比较对象以确定排序顺序。
  • TreeSet不synchronized 。 如果多个线程同时访问哈希集,并且至少有一个线程修改了哈希集,则必须在外部对其进行同步。
  • 使用Collections.synchronizedSortedSet(new TreeSet())方法获取同步的TreeSet。

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
boolean add(E e) :将指定的元素添加到Set中(如果尚不存在)。
Comparator comparator() :返回用于对该集合中的元素进行排序的Comparator comparator()如果此集合使用其元素的自然排序,则返回null
Object first() :返回当前在此集合中的第一个(最低)元素。
Object last() :返回当前在此集合中的最后一个(最大)元素。
void clear() :从TreeSet中删除所有元素。
boolean contains(Object o) :如果TreeSet包含指定的元素,则返回true否则返回false
boolean isEmpty() :如果TreeSet不包含任何元素,则返回true ,否则返回false
int size() :返回TreeSet中的元素数。
Iterator<E> iterator() :以ascending order返回此集合中元素的迭代器。
Iterator<E> descendingIterator() :以Iterator<E> descendingIterator()返回此集合中元素的迭代器。
NavigableSet<E> descendingSet() :返回此集合中包含的元素的逆序视图。
boolean remove(Object o) :从TreeSet中移除指定的元素(如果存在)并返回true ,否则返回false
Object clone() :返回TreeSet的浅表副本。
Spliterator<E> spliterator() :在此TreeSet中的元素上创建后绑定和故障快速的Spliterator。 它与树集提供的顺序相同。