JAVA集合面试分享十:JAVA集合框架详解之Set-阶段(三)

目录

一、HashSet

1. 说明

2. 底层机制说明

3. 分析扩容和转成红黑树机制

二、TreeSet

1、简介

2、排序方式

三、LinkedHashSet

1.    LinkedHashSet概述:

2.   LinkedHashSet的实现:

四、CopyOnWriteArraySet

1、简介

2、特性

3、数据结构 

五、ConcurrentSkipListSet

1、 基本概念

2、场景特点

2、内部原理

3、构造函数


先给大家看一张图,有个大致了解:

再来看下Set的继承关系:

Set实现了以下几种:

  • HashSet
  • TreeSet
  • LinkedHashSet
  • CopyOnWriteArraySet
  • ConcurrentSkipListSet

一、HashSet

1. 说明
  1. HashSet实现了Set接口
  2. HashSet底层实质上是HashMap
  3. 可以存放null值,但是只能有一个null
  4. HashSet不保证元素是有序的,取决于hash后,再确定索引的结果,即不保证存放元素的顺序和取出顺序一致
  5. 不能有重复元素/对象
2. 底层机制说明

HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)

  1. 先获取元素的哈希值(hashcode方法)
  2. 对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
  3. 如果该位置上没有其他元素,则直接存放,如果该位置上有其他元素,则需要进行equals判断,如果相等,则不再添加,如果不相等,则以链表的方式添加
  4. Java8以后,如果一条链表中的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行数化(红黑树)
3. 分析扩容和转成红黑树机制
  1. HashSet底层是HashMap,第一次添加时,table的数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor是0.75)=12
  2. 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,依次类推
  3. 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)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值