Java基础 set集合

Java Set集合核心知识点总结(HashSet/LinkedHashSet/TreeSet/ConcurrentSkipListSet)

在Java集合框架中,Set是继承自Collection的顶级接口之一,代表元素不可重复、无索引的集合。与List允许重复元素且支持索引访问不同,Set的核心特性是唯一性,通过底层哈希表或红黑树保证元素不重复。本文将从底层原理、源码解析、性能对比、面试陷阱等维度深入梳理Set的四大核心实现类,帮你彻底掌握Set集合的使用与选型。

一、HashSet:基于哈希表的无序去重实现

1. 核心特点

  • 底层完全基于HashMap实现,所有元素存储在HashMap的key中,value是一个固定的静态Object常量
  • 无序:不保证元素的插入顺序,也不保证顺序随时间不变
  • 元素不可重复:通过key的唯一性保证,重复元素添加失败
  • 允许一个null元素(因为HashMap允许一个null键)
  • 线程不安全:多线程环境下操作会出现并发修改异常
  • 综合性能最高,是日常开发中最常用的Set实现

2. 底层原理(重点)

HashSet的本质是一个"阉割版"的HashMap,所有核心操作都委托给HashMap完成:

java 复制代码
// HashSet的核心成员变量
private transient HashMap<E,Object> map;
// 用于填充HashMap value的静态常量
private static final Object PRESENT = new Object();

// 无参构造器:创建默认容量16、负载因子0.75的HashMap
public HashSet() {
    map = new HashMap<>();
}

// add方法核心实现:调用HashMap的put方法
public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}
  • 去重原理 :当调用add(e)时,HashMap会先计算e的hashCode定位数组索引,再通过equals()方法比较元素是否相等。如果两个元素hashCode相同且equals返回true,则视为重复元素,put方法返回旧值,add方法返回false表示添加失败。
  • 扩容机制:完全继承HashMap的扩容规则,初始容量16,负载因子0.75,扩容为原容量的2倍。

3. 常用方法与示例

(1)基本操作
java 复制代码
HashSet<String> set = new HashSet<>();
// 添加元素,重复元素添加失败返回false
set.add("a");
set.add("b");
set.add("a"); // 返回false,添加失败
System.out.println(set); // 输出:[a, b](顺序不定)

// 获取元素数量
System.out.println(set.size()); // 输出:2

// 判断元素是否存在
boolean hasQingcheng = set.contains("a"); // 输出:true

// 删除元素,删除成功返回true
set.remove("b"); // 输出:true

// 清空集合
set.clear();
// 判断是否为空
System.out.println(set.isEmpty()); // 输出:true
(2)遍历方式
java 复制代码
// 1. 增强for循环(推荐,简洁)
for (String s : set) {
    System.out.println(s);
}

// 2. 迭代器遍历(唯一支持遍历过程中安全删除的方式)
Iterator<String> it = set.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("b")) {
        it.remove(); // 安全删除
    }
}

// 3. Stream遍历(Java 8+)
set.stream().forEach(System.out::println);

4. 常见陷阱与注意事项

  1. 必须正确重写元素的hashCode()和equals()方法:这是HashSet保证元素唯一性的核心。如果只重写equals不重写hashCode,会导致相同内容的对象被视为不同元素;反之则会导致哈希冲突严重,性能下降。
  2. 避免使用可变对象作为Set元素:如果元素的内容发生变化,其hashCode也会改变,导致HashSet无法找到该元素,造成内存泄漏。
  3. 初始容量优化 :如果提前知道元素数量,创建HashSet时指定初始容量(建议预计元素数 / 0.75 + 1),避免频繁扩容带来的性能损耗。

二、LinkedHashSet:维护插入顺序的哈希表实现

1. 核心特点

  • 继承自HashSet,底层基于LinkedHashMap实现,在HashMap的数组+链表+红黑树结构基础上,额外维护了一条双向链表记录元素的插入顺序
  • 有序:严格按照元素的插入顺序进行存储和遍历
  • 元素不可重复:与HashSet一致,通过HashMap的key唯一性保证
  • 允许一个null元素
  • 线程不安全
  • 性能略低于HashSet(因为要维护双向链表),但遍历性能更高(按链表顺序遍历,无需遍历整个数组)

2. 底层原理

LinkedHashSet的构造器会调用父类HashSet的特定构造器,创建一个LinkedHashMap实例:

java 复制代码
// HashSet的受保护构造器,供LinkedHashSet调用
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

// LinkedHashSet无参构造器
public LinkedHashSet() {
    super(16, .75f, true);
}
  • LinkedHashMap的双向链表会记录每个节点的前驱和后继节点,从而保证插入顺序。
  • 去重机制与HashSet完全一致,同样依赖hashCode和equals方法。

3. 常用方法与示例

LinkedHashSet的方法与HashSet完全相同,唯一区别是遍历顺序与插入顺序一致:

java 复制代码
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Cherry");
set.add("Apple"); // 重复元素,添加失败

System.out.println(set); // 输出:[Apple, Banana, Cherry](严格按插入顺序)

// 集合运算:交集、差集
LinkedHashSet<String> set1 = new LinkedHashSet<>(Arrays.asList("A", "B", "C"));
LinkedHashSet<String> set2 = new LinkedHashSet<>(Arrays.asList("B", "C", "D"));

// 交集:保留两个集合共有的元素
set1.retainAll(set2);
System.out.println(set1); // 输出:[B, C]

// 差集:删除set1中与set2共有的元素
set1.removeAll(set2);
System.out.println(set1); // 输出:[]

4. 适用场景

当需要去重且保持元素的插入顺序时使用,例如:

  • 记录用户的浏览历史(去重且按浏览顺序展示)
  • 实现简单的LRU缓存(结合LinkedHashMap的访问顺序模式)

三、TreeSet:基于红黑树的有序去重实现

1. 核心特点

  • 底层基于TreeMap实现,所有元素存储在TreeMap的key中
  • 有序:支持两种排序方式------自然排序(默认)和自定义排序
  • 元素不可重复:通过排序规则的比较结果为0来判断元素重复,而非hashCode和equals
  • 不允许null元素(因为需要参与排序比较)
  • 线程不安全
  • 插入、删除、查询的时间复杂度均为O(logn),性能低于HashSet,但支持有序操作

2. 排序规则

TreeSet的排序优先级:自定义排序 > 自然排序

  1. 自然排序 :元素必须实现Comparable接口,重写compareTo()方法。String、Integer等包装类已默认实现该接口:

    • Integer:按数值升序
    • String:按字典序升序
    • Date:按时间戳升序
  2. 自定义排序 :创建TreeSet时传入Comparator接口的实现类,重写compare()方法,优先级高于自然排序。

3. 常用方法与示例

(1)自然排序
java 复制代码
// Integer自然排序(数值升序)
TreeSet<Integer> numSet = new TreeSet<>();
numSet.add(3);
numSet.add(1);
numSet.add(2);
System.out.println(numSet); // 输出:[1, 2, 3]

// String自然排序(字典序)
TreeSet<String> strSet = new TreeSet<>();
strSet.add("agg");
strSet.add("abcd");
strSet.add("ffas");
System.out.println(strSet); // 输出:[abcd, agg, ffas]
(2)自定义排序
java 复制代码
// 自定义排序:按Person的年龄升序,年龄相同按姓名字典序
TreeSet<Person> personSet = new TreeSet<>((p1, p2) -> {
    if (p1.getAge() != p2.getAge()) {
        return p1.getAge() - p2.getAge();
    } else {
        return p1.getName().compareTo(p2.getName());
    }
});

personSet.add(new Person("agg", 21));
personSet.add(new Person("abcd", 12));
personSet.add(new Person("ffas", 8));
personSet.add(new Person("agg", 12));

for (Person p : personSet) {
    System.out.println(p.getName() + ":" + p.getAge());
}
// 输出:
// ffas:8
// abcd:12
// agg:12
// agg:21
(3)特有有序方法
java 复制代码
TreeSet<Integer> set = new TreeSet<>(Arrays.asList(1, 2, 3, 4, 5));

// 获取最小/最大元素
System.out.println(set.first()); // 输出:1
System.out.println(set.last()); // 输出:5

// 获取小于等于指定值的最大元素
System.out.println(set.floor(3)); // 输出:3
// 获取大于等于指定值的最小元素
System.out.println(set.ceiling(3)); // 输出:3

// 获取子集合:[fromElement, toElement)
SortedSet<Integer> subSet = set.subSet(2, 4);
System.out.println(subSet); // 输出:[2, 3]

// 获取小于指定值的子集合
SortedSet<Integer> headSet = set.headSet(3);
System.out.println(headSet); // 输出:[1, 2]

// 获取大于等于指定值的子集合
SortedSet<Integer> tailSet = set.tailSet(3);
System.out.println(tailSet); // 输出:[3, 4, 5]

4. 注意事项

  1. 排序规则必须与equals()保持一致 :如果两个元素通过compareTo()compare()方法比较返回0,但equals()返回false,TreeSet会认为它们是重复元素,导致其中一个无法添加。
  2. 不允许null元素 :因为排序时需要调用元素的compareTo()方法,null会抛出NullPointerException
  3. 性能权衡:TreeSet的插入和删除操作需要维护红黑树的平衡,性能比HashSet低约5-10倍,仅在需要有序操作时使用。

四、ConcurrentSkipListSet:高并发场景的有序Set实现

1. 核心特点

  • 底层基于ConcurrentSkipListMap实现,采用跳表(Skip List)数据结构
  • 有序:支持自然排序和自定义排序,与TreeSet一致
  • 线程安全:通过CAS和volatile保证并发操作的安全性,无需加锁
  • 元素不可重复:通过排序规则判断重复
  • 不允许null元素
  • 并发性能远高于Collections.synchronizedSortedSet包装的TreeSet,是高并发场景下有序Set的唯一选择

2. 核心原理

跳表是一种基于多层索引的有序数据结构,通过空间换时间,将查询时间复杂度从O(n)优化为O(logn)。ConcurrentSkipListSet采用无锁算法,支持多个线程并发读写,读操作完全无锁,写操作仅影响局部节点,并发性能优异。

3. 适用场景

  • 高并发环境下需要有序去重的场景
  • 替代线程不安全的TreeSet和性能低下的Collections.synchronizedSortedSet

五、Set集合核心总结与选型对比

1. Set接口通用特性

  • 元素不可重复,通过底层机制保证唯一性
  • 无索引,不支持通过索引访问元素
  • 继承自Collection接口,支持集合的通用操作(添加、删除、查询、遍历)
  • 所有实现类的迭代器都是快速失败(fail-fast)的,遍历过程中修改集合会抛出ConcurrentModificationException(ConcurrentSkipListSet除外,是弱一致性迭代器)

2. 四大实现类对比表

特性 HashSet LinkedHashSet TreeSet ConcurrentSkipListSet
底层数据结构 HashMap(数组+链表+红黑树) LinkedHashMap(数组+双向链表+红黑树) TreeMap(红黑树) ConcurrentSkipListMap(跳表)
有序性 无序 插入顺序 排序顺序(自然/自定义) 排序顺序(自然/自定义)
null元素支持 允许1个 允许1个 不允许 不允许
线程安全 不安全 不安全 不安全 安全(无锁并发)
平均时间复杂度 O(1) O(1) O(logn) O(logn)
插入性能 极高 中等 高(并发场景)
遍历性能 中等 极高 中等 中等
适用场景 单线程通用去重场景 需要保持插入顺序的去重 需要有序排序的去重 高并发有序去重场景
综合性能 最高 中等 高(并发场景)

3. List与Set的核心区别

特性 List Set
有序性 严格按插入顺序排序 大部分实现无序(除LinkedHashSet、TreeSet)
元素重复性 允许重复元素 不允许重复元素
索引支持 支持通过int索引访问元素 不支持索引访问
去重机制 无,需手动判断 底层自动保证唯一性
遍历方式 普通for循环、增强for、迭代器 增强for、迭代器、Stream
常用实现类 ArrayList、LinkedList HashSet、LinkedHashSet、TreeSet
适用场景 频繁查询、需要索引访问 去重、集合运算

结论

Set集合是Java开发中实现元素去重和集合运算的核心工具,掌握其底层原理和选型技巧能显著提升代码质量和运行性能。日常开发中:

  • 绝大多数单线程去重场景优先使用HashSet,性能最高
  • 当需要去重且保持插入顺序时,选择LinkedHashSet
  • 当需要对元素进行排序时,选择TreeSet
  • 高并发场景下需要有序去重时,选择ConcurrentSkipListSet
  • 永远不要使用Collections.synchronizedSet包装的HashSet或TreeSet,性能远低于专门的并发实现
相关推荐
驭渊的小故事3 小时前
继承和多态
java·开发语言
天天打码3 小时前
从 Rolldown 到 Oxc:前端工具链正在全面 Rust 化
开发语言·前端·rust
Bechamz3 小时前
大数据开发学习Day27
java·大数据·学习
Byron__3 小时前
Java并发核心面试知识点
java·面试·多线程·并发编程
Java成神之路-3 小时前
Java SPI vs Spring SPI
java·spring
希望永不加班3 小时前
Java数据类型陷阱:int和Integer的7个关键区别
java·开发语言
boonya3 小时前
Idea CC GUI插件如何通过 CC Switch 工具将 Claude Code 的后端配置为 DeepSeek 的 v4-pro 模型?
java·ide·intellij-idea
The Chosen One9853 小时前
【Linux】深入理解Linux进程(二):进程的状态
linux·运维·服务器·开发语言·git
花千树-0103 小时前
从业务接口到 MCP Tool:多语言工程化实践指南(Python / TypeScript / Java)
java·python·rpc·typescript·api·mcp