Files
notes_estom/Java/03Java集合类/05 Set.md
yinkanglong cc62258411 java
2023-10-21 09:55:22 +08:00

16 KiB
Raw Blame History

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() 方法,所以使用抽象类把该方法重写后,子类无需关心这两个方法。

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 的基础上扩展了排序的行为,所以所有实现它的子类都会拥有排序功能。

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

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 的。

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)传递默认容量来覆盖此默认容量。
public boolean addE e 如果指定的元素不存在则将其添加到Set中 此方法在内部使用equals()方法检查重复项 如果元素重复则元素被拒绝并且不替换值
public void clear() 从哈希集中删除所有元素
public boolean containsObject o 如果哈希集包含指定的元素othrweise false 则返回false 
public boolean isEmpty() 如果哈希集不包含任何元素则返回true 否则返回false 
public int size() 返回哈希集中的元素数量
public Iterator<E> iterator() 返回对此哈希集中的元素的迭代器 从迭代器返回的元素没有特定的顺序
public boolean removeObject o 从哈希集中删除指定的元素如果存在并返回true 否则返回false 
public boolean removeAllCollection <> c 删除哈希集中属于指定集合的所有元素
public Object clone() 返回哈希集的浅表副本
public Spliterator<E> spliterator() 在此哈希集中的元素上创建后绑定和故障快速的Spliterator

2 LinkedHashSet

底层原理

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

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

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。

使用方法

public boolean addE e 如果指定的元素不存在则将其添加到Set中 此方法在内部使用equals()方法检查重复项 如果元素重复则元素被拒绝并且不替换值
public void clear() 从LinkedHashSet中删除所有元素
public boolean containsObject o 如果LinkedHashSet包含指定的元素othrweise false 则返回false 
public boolean isEmpty() 如果LinkedHashSet不包含任何元素则返回true 否则返回false 
public int size() 返回LinkedHashSet中的元素数
public Iterator<E> iterator() 返回对此LinkedHashSet中的元素的迭代器 从迭代器返回的元素没有特定的顺序
public boolean removeObject o 从LinkedHashSet中移除指定的元素如果存在并返回true 否则返回false 
public boolean removeAllCollection <> 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

public TreeSet() { 
    this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}

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

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。

使用方法

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 它与树集提供的顺序相同

4 CopyOnWriteArraySet

底层原理

HashSet的thread-safe变体它对所有操作都使用基础CopyOnWriteArrayList

与CopyOnWriteArrayList相似它的immutable snapshot样式iterator方法在创建iterator使用对数组状态在后备列表内的引用。 这在遍历操作远远超过集合更新操作且我们不想同步遍历并且在更新集合时仍希望线程安全的用例中很有用。

  • 作为正常设置的数据结构,它不允许重复。
  • CopyOnWriteArraySet类实现Serializable接口并扩展AbstractSet类。
  • 使用CopyOnWriteArraySet进行更新操作成本很高因为每个突变都会创建基础数组的克隆副本并向其添加/更新元素。
  • 它是HashSet的线程安全版本。 每个访问该集合的线程在初始化此集合的迭代器时都会看到自己创建的后备阵列快照版本。
  • 因为它在创建迭代器时获取基础数组的快照所以它不会抛出ConcurrentModificationException 。不支持迭代器上的变异操作。 这些方法抛出UnsupportedOperationException 。
  • CopyOnWriteArraySet是synchronized Set的并发替代当迭代的次数超过突变次数时CopyOnWriteArraySet提供更好的并发性。
  • 它允许重复的元素和异构对象(使用泛型来获取编译时错误)。
  • 由于每次创建迭代器时都会创建基础数组的新副本因此performance is slower HashSet

主要方法

CopyOnWriteArraySet() 创建一个空集
CopyOnWriteArraySet(Collection c) 创建一个包含指定集合元素的集合其顺序由集合的迭代器返回
boolean add(object o) 将指定的元素添加到此集合如果尚不存在)。
boolean addAll(collection c) 将指定集合中的所有元素如果尚不存在boolean addAll(collection c)添加到此集合中
void clear() 从此集合中删除所有元素
boolean contains(Object o) 如果此集合包含指定的元素则返回true
boolean isEmpty() 如果此集合不包含任何元素则返回true
Iterator iterator() 以添加这些元素的顺序在此集合中包含的元素上返回一个迭代器
boolean remove(Object o) 从指定的集合中删除指定的元素如果存在)。
int size() 返回此集合中的元素数

实例

CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>(Arrays.asList(1,2,3));
 
System.out.println(set);    //[1, 2, 3]
 
//Get iterator 1
Iterator<Integer> itr1 = set.iterator();
 
//Add one element and verify set is updated
set.add(4);
System.out.println(set);    //[1, 2, 3, 4]
 
//Get iterator 2
Iterator<Integer> itr2 = set.iterator();
 
System.out.println("====Verify Iterator 1 content====");
 
itr1.forEachRemaining(System.out :: println);   //1,2,3
 
System.out.println("====Verify Iterator 2 content====");
 
itr2.forEachRemaining(System.out :: println);   //1,2,3,4