Set详解

107 阅读5分钟

Java Set实现类包括HashSet(哈希表去重)、LinkedHashSet(维护插入顺序)、TreeSet(红黑树排序)、CopyOnWriteArraySet(写时复制线程安全)和ConcurrentSkipListSet(跳表高并发排序),适用于不同场景。

一、Set 接口核心特性

  • 唯一性:所有元素唯一,不允许重复(通过 equals()hashCode() 判断)。
  • 无序性(默认):大多数实现类不保证顺序(TreeSetLinkedHashSet 例外)。
  • 允许 null:大多数实现类允许单个 null 元素(ConcurrentSkipListSet 不允许)。
  • 动态扩容:自动处理容量扩展(基于底层数据结构)。

二、Set 主要实现类对比

实现类底层数据结构线程安全元素顺序时间复杂度(插入/查询)适用场景
HashSet哈希表(基于 HashMap非线程安全无序O(1)快速去重、无需顺序
LinkedHashSet哈希表 + 双向链表非线程安全插入顺序O(1)需要保留插入顺序的去重
TreeSet红黑树(基于 TreeMap非线程安全自然排序/定制排序O(log n)需要排序或范围查询
CopyOnWriteArraySet动态数组(写时复制)线程安全无序O(n)(写)、O(1)(读)高并发读、极少写操作
ConcurrentSkipListSet跳表(Skip List)线程安全自然排序/定制排序O(log n)高并发且需要排序的查询/写入

三、各实现类详解

1. HashSet
  • 底层数据结构:基于 HashMap(元素作为 HashMap 的 key,value 统一为 PRESENT 静态对象)。

    // JDK 17 源码核心片段
    public class HashSet<E> extends AbstractSet<E> {
        private transient HashMap<E, Object> map;
        private static final Object PRESENT = new Object();
        
        public boolean add(E e) {
            return map.put(e, PRESENT) == null; // 利用 HashMap 的 key 唯一性
        }
    }
    
  • 扩容机制:与 HashMap 一致,默认初始容量 16,负载因子 0.75,扩容至 2 倍。

  • 线程安全:非线程安全,需通过 Collections.synchronizedSet() 包装。

  • 优点

    • 插入、删除、查询效率高(哈希表直接定位)。
    • 内存占用较低(相比 LinkedHashSet)。
  • 缺点

    • 不保证顺序。
    • 哈希冲突可能影响性能(链表过长或树化)。

2. LinkedHashSet
  • 底层数据结构:继承自 HashSet,内部通过 LinkedHashMap 实现。

    public class LinkedHashSet<E> extends HashSet<E> {
        public LinkedHashSet() {
            super(16, 0.75f, true); // 调用 HashSet 的特定构造方法
        }
    }
    ​
    // HashSet 中隐藏的构造方法
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        this.map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
    
  • 顺序维护:通过双向链表维护插入顺序(或访问顺序,若 accessOrder 为 true)。

  • 扩容机制:同 HashSet,基于 LinkedHashMap 的哈希表扩容。

  • 优点

    • 保留插入顺序,适合需要顺序遍历的场景。
    • 查询效率接近 HashSet
  • 缺点

    • 内存占用略高(需维护链表指针)。
    • 插入性能略低于 HashSet(需更新链表)。

3. TreeSet
  • 底层数据结构:基于 TreeMap(红黑树实现)。

    public class TreeSet<E> extends AbstractSet<E> {
        private transient NavigableMap<E, Object> map;
        private static final Object PRESENT = new Object();
        
        public boolean add(E e) {
            return map.put(e, PRESENT) == null; // 红黑树插入
        }
    }
    
  • 排序规则

    • 自然排序:元素实现 Comparable 接口。
    • 定制排序:通过 Comparator 传入构造函数。
  • 操作复杂度:插入、删除、查询均为 O(log n) (红黑树高度平衡)。

  • 线程安全:非线程安全,需外部同步。

  • 优点

    • 支持范围查询(如 subSet(), headSet(), tailSet())。
    • 自动排序,适合需要有序集合的场景。
  • 缺点

    • 插入和删除效率低于 HashSet
    • 内存占用较高(红黑树节点结构复杂)。

4. CopyOnWriteArraySet
  • 底层数据结构:基于 CopyOnWriteArrayList,通过动态数组实现写时复制。

    public class CopyOnWriteArraySet<E> extends AbstractSet<E> {
        private final CopyOnWriteArrayList<E> al;
        
        public boolean add(E e) {
            return al.addIfAbsent(e); // 写时复制保证唯一性
        }
    }
    
  • 唯一性保证:在写入时遍历数组检查元素是否存在(addIfAbsent())。

  • 线程安全:通过 ReentrantLock 保证写操作线程安全,读操作无锁。

  • 优点

    • 读性能极高(无锁访问数组快照)。
    • 适合读多写极少的高并发场景。
  • 缺点

    • 写操作性能差(需复制整个数组)。
    • 数据一致性弱(迭代器访问旧快照)。

5. ConcurrentSkipListSet
  • 底层数据结构:基于跳表(Skip List),实现高并发排序。

    public class ConcurrentSkipListSet<E> extends AbstractSet<E> {
        private final ConcurrentNavigableMap<E, Object> map;
        private static final Object PRESENT = new Object();
        
        public boolean add(E e) {
            return map.put(e, PRESENT) == null; // 跳表插入
        }
    }
    
  • 跳表结构

    • 多层索引链表,通过概率平衡(类似平衡树)。
    • 支持快速查找、插入、删除(时间复杂度 O(log n))。
  • 线程安全:通过 CAS(Compare-And-Swap)和无锁算法实现高并发。

  • 优点

    • 高并发下性能优于 TreeSet
    • 支持自然排序和范围查询。
  • 缺点

    • 内存占用高(多层索引结构)。
    • 实现复杂,维护成本高。

四、扩容机制对比

实现类初始容量扩容规则扩容触发条件
HashSet16HashMap(2 倍扩容)元素数量 > 容量 * 负载因子
LinkedHashSet16HashSetHashSet
TreeSet无固定容量动态扩展红黑树节点无需显式扩容
CopyOnWriteArraySet0每次写操作复制数组并扩展1每次添加元素时触发
ConcurrentSkipListSet无固定容量跳表动态扩展层数自动调整

五、线程安全方案对比

实现类线程安全实现方式锁粒度适用场景
CopyOnWriteArraySet写时复制 + ReentrantLock全局锁(写操作)读多写极少的高并发场景
ConcurrentSkipListSetCAS + 无锁跳表无锁(细粒度)高并发读写且需要排序的场景
Collections.synchronizedSet对象锁(synchronized)粗粒度简单同步需求

六、数据结构图示(Mermaid)

ConcurrentSkipListSet
多层索引
高并发排序
跳表
CopyOnWriteArraySet
读无锁
写复制
写时复制数组
TreeSet
自动排序
支持范围查询
红黑树
LinkedHashSet
维护插入顺序
哈希表+双向链表
HashSet
数组+链表/红黑树
哈希表

七、选择建议

  • 单线程场景

    • 快速去重HashSet
    • 需要插入顺序LinkedHashSet
    • 需要排序或范围查询TreeSet
  • 高并发场景

    • 读多写极少CopyOnWriteArraySet
    • 读写均衡且需要排序ConcurrentSkipListSet
  • 避免使用:直接使用 Collections.synchronizedSet() 包装的集合(除非简单同步需求)。


八、示例代码

// TreeSet 自然排序示例
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(3);
treeSet.add(1);
treeSet.add(2);
System.out.println(treeSet); // 输出 [1, 2, 3]
​
// ConcurrentSkipListSet 高并发写入
Set<String> concurrentSet = new ConcurrentSkipListSet<>();
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> concurrentSet.add("task-" + ThreadLocalRandom.current().nextInt()));
}
executor.shutdown();

九、总结

  • HashSet:性能最优的无序去重集合。
  • LinkedHashSet:兼顾插入顺序和查询效率。
  • TreeSet:唯一支持自动排序和范围查询的集合。
  • CopyOnWriteArraySet:读多写极少场景的线程安全选择。
  • ConcurrentSkipListSet:高并发排序场景的终极方案。