Java Set 集合全解析:从原理到实战的系统梳理

Set 集合作为 Java 集合框架的重要分支,以 “无序、不重复、无索引” 为核心特性,在数据去重、排序筛选等场景中应用广泛。本文将从特性对比、实现类原理、底层机制到实际案例,全面梳理 Set 集合的知识体系,助你吃透 Set 的设计思想与使用技巧。

一、Set 集合核心认知:与 List 的本质区别

在学习 Set 之前,必须先明确其与 List 集合的核心差异 —— 这是正确选择集合类型的前提。通过表格可直观对比二者的核心区别:

集合类型

核心特性

关键能力

典型场景

List

有序、可重复、有索引

支持get(int index)索引操作

存储有序数据(如列表展示)

Set

无序、不重复、无索引

自动去重,不支持索引操作

存储唯一标识(如用户 ID)

核心特性解读

  • 无序:元素的存储顺序与添加顺序无关(特殊实现类除外);
  • 不重复:添加重复元素时,add(E e)方法返回false,集合不新增元素;
  • 无索引:无法通过 “位置” 操作元素,因此不能使用普通for循环遍历。

二、Set 三大实现类深度解析:原理、特性与场景

Set 是接口,需通过实现类实例化。Java 提供HashSet、LinkedHashSet、TreeSet三大常用实现类,三者底层机制不同,适用场景各有侧重。

1. HashSet:基于哈希表,性能最优的去重方案

HashSet是 Set 最常用的实现类,其设计核心是 “哈希表”,追求极致的增删改查性能,是大多数去重场景的首选。

(1)底层结构:从 “数组 + 链表” 到 “数组 + 链表 + 红黑树”

JDK 对HashSet的底层实现进行过优化,不同版本的结构差异如下:

  • JDK8 之前:采用 “数组 + 链表” 结构,通过链表解决哈希碰撞问题;
  • JDK8 及之后:升级为 “数组 + 链表 + 红黑树”,当链表长度超过 8 且数组长度≥64 时,链表会自动转为红黑树,大幅优化查询性能。
(2)核心概念:哈希值与哈希碰撞
  • 哈希值:对象的整数表现形式,通过hashCode()方法计算。Object 类默认基于对象地址值计算哈希值,自定义对象通常需重写该方法,基于内部属性值计算;
  • 哈希碰撞:不同对象计算出相同哈希值的情况(概率极低),通过链表或红黑树可有效解决。
(3)去重原理:hashCode()与equals()的协同

HashSet的去重逻辑是面试高频考点,核心依赖hashCode()和equals()两个方法的协同工作,具体步骤如下:

  1. 计算新增元素的哈希值,确定其在数组中的存储位置;
  1. 若该位置为null,直接将元素存入;
  1. 若位置不为null,调用equals()方法比较元素内容:
    • 若equals()返回true:判定为重复元素,不存入;
    • 若equals()返回false:判定为不同元素,存入链表或红黑树。
(4)关键避坑点

存储自定义对象时,必须同时重写hashCode()和equals()方法,否则无法正确去重。重写需遵循以下原则:

  • 属性相同的对象,hashCode()返回值必须相同;
  • hashCode()相同的对象,equals()必须返回true。
(5)特性与适用场景
  • 特性:无序、不重复、无索引,线程不安全,增删查效率接近 O (1);
  • 适用场景:无需关注元素顺序,仅需去重的场景(如存储商品编号、订单 ID、用户手机号等唯一标识)。

2. LinkedHashSet:哈希表 + 链表,兼顾顺序与去重

LinkedHashSet是HashSet的子类,核心改进是在哈希表基础上增加 “双向链表”,解决了HashSet的无序问题,实现 “存取顺序一致”。

(1)底层结构:哈希表 + 双向链表
  • 哈希表:继承自HashSet,保证去重能力和高效的增删查性能;
  • 双向链表:额外维护元素的添加顺序,使元素遍历顺序与添加顺序一致。
(2)核心特性
  • 有序:元素的遍历顺序与添加顺序完全一致;
  • 继承性:完全保留HashSet的去重、无索引特性;
  • 性能:略低于HashSet(因维护双向链表存在额外开销),但远高于其他有序集合。
(3)适用场景

需要去重且需保留元素添加顺序的场景(如记录用户访问路径、日志打印顺序、操作历史记录等)。

3. TreeSet:基于红黑树,可排序的去重方案

TreeSet底层基于红黑树(一种自平衡二叉搜索树)实现,核心特性是 “可排序”,通过牺牲部分性能换取排序能力,适用于需排序的去重场景。

(1)排序的两种实现方式

TreeSet的排序逻辑分为 “自然排序” 和 “比较器排序”,二者优先级为:比较器排序>自然排序。

① 自然排序:元素类实现Comparable接口

自然排序要求存储的元素类实现Comparable<T>接口,并重写compareTo(T o)方法定义排序规则。该方法的返回值规则如下:

  • 返回正数:当前元素排在参数元素之后;
  • 返回负数:当前元素排在参数元素之前;
  • 返回 0:判定为重复元素,不存入集合。

示例:学生按年龄升序排序,年龄相同则按姓名字典序排序


class Student implements Comparable<Student> {

private String name;

private Integer age;

public Student(String name, Integer age) {

this.name = name;

this.age = age;

}

@Override

public int compareTo(Student o) {

// 按年龄升序

int ageCompare = this.age - o.age;

// 年龄相同则按姓名字典序升序

return ageCompare != 0 ? ageCompare : this.name.compareTo(o.name);

}

}

// 使用自然排序创建TreeSet

Set<Student> treeSet = new TreeSet<>();

treeSet.add(new Student("张三", 18));

treeSet.add(new Student("李四", 17));

treeSet.add(new Student("王五", 18));
② 比较器排序:创建TreeSet时传入Comparator

当无法修改元素类(如 String、Integer 等系统类),或需临时变更排序规则时,可使用比较器排序。创建TreeSet时传入Comparator对象,通过匿名内部类或 Lambda 表达式定义排序逻辑。

示例:按字符串长度降序排序,长度相同则按字典序升序


Set<String> treeSet = new TreeSet<>((s1, s2) -> {

// 按长度降序

int lenCompare = s2.length() - s1.length();

// 长度相同按字典序升序

return lenCompare != 0 ? lenCompare : s1.compareTo(s2);

});

treeSet.add("apple");

treeSet.add("banana");

treeSet.add("cherry");

System.out.println(treeSet); // 输出:[banana, cherry, apple]
(2)特性与适用场景
  • 特性:可排序、不重复、无索引,线程不安全,增删查效率为 O (logN);
  • 适用场景:需要去重且需对元素进行排序的场景(如学生成绩排名、商品价格排序、数据按时间戳排序等)。

三、Set 集合通用操作:创建、遍历与常用 API

Set 的操作与 Collection 接口基本一致,核心操作可分为 “基本管理” 和 “遍历” 两大类。

1. 基本操作:创建与元素管理


// 1. 创建集合(以HashSet为例,其他实现类用法类似)

Set<String> set = new HashSet<>();

// 2. 添加元素:add(E e),返回boolean(添加成功true/重复false)

boolean isAdded1 = set.add("张三"); // true

boolean isAdded2 = set.add("张三"); // false(重复,添加失败)

set.add("李四");

set.add("王五");

// 3. 删除元素:remove(Object o),返回boolean(存在true/不存在false)

boolean isRemoved = set.remove("王五"); // true

// 4. 判断与查询

boolean hasLiSi = set.contains("李四"); // 判断是否包含指定元素:true

int size = set.size(); // 获取元素个数:2

boolean isEmpty = set.isEmpty(); // 判断是否为空:false

// 5. 清空集合

set.clear();

2. 遍历方式:三种常用方案(无普通 for 循环)

由于 Set 无索引,无法使用普通for循环遍历,常用的遍历方式有以下三种:

(1)迭代器(Iterator)遍历:通用且支持安全删除

迭代器是 Collection 接口的通用遍历工具,支持在遍历过程中安全删除元素,避免ConcurrentModificationException异常。


Iterator<String> iterator = set.iterator();

while (iterator.hasNext()) {

String element = iterator.next();

System.out.println(element);

// 遍历中安全删除元素

if (element.equals("李四")) {

iterator.remove();

}

}
(2)增强 for 循环(foreach):简洁高效

增强 for 循环是遍历集合的简化写法,代码简洁,无需关注迭代器的具体操作。


for (String element : set) {

System.out.println(element);

}
(3)Lambda 表达式遍历:函数式编程风格

Java 8 及以上版本支持通过forEach(Consumer<? super E> action)方法结合 Lambda 表达式遍历,符合函数式编程思想,代码更简洁。


// 基础Lambda写法

set.forEach(element -> System.out.println(element));

// 方法引用简化(仅调用单一方法时)

set.forEach(System.out::println);

四、实战案例:HashSet 存储自定义对象去重

下面通过完整案例演示如何使用HashSet存储自定义对象并实现正确去重。

1. 完整代码实现


import java.util.HashSet;

import java.util.Objects;

// 自定义Student类(需重写hashCode和equals实现去重)

class Student {

private String name;

private Integer age;

// 构造方法

public Student(String name, Integer age) {

this.name = name;

this.age = age;

}

// 重写hashCode:基于name和age计算哈希值

@Override

public int hashCode() {

return Objects.hash(name, age);

}

// 重写equals:name和age相同则视为同一对象

@Override

public boolean equals(Object o) {

if (this == o) return true;

if (o == null || getClass() != o.getClass()) return false;

Student student = (Student) o;

return Objects.equals(name, student.name) && Objects.equals(age, student.age);

}

// getter方法:用于遍历输出

public String getName() {

return name;

}

public Integer getAge() {

return age;

}

}

// 测试类

public class HashSetCustomObjectDemo {

public static void main(String[] args) {

// 创建HashSet集合

HashSet<Student> students = new HashSet<>();

// 添加元素(s1和s2属性相同,视为重复)

Student s1 = new Student("张三", 18);

Student s2 = new Student("张三", 18);

Student s3 = new Student("李四", 19);

Student s4 = new Student("王五", 18);

students.add(s1);

students.add(s2);

students.add(s3);

students.add(s4);

// 遍历集合(仅3个元素,去重成功)

System.out.println("去重后的学生列表:");

for (Student student : students) {

System.out.println("姓名:" + student.getName() + ",年龄:" + student.getAge());

}

}

}

2. 运行结果与说明

  • 运行结果:仅输出 “张三(18)、李四(19)、王五(18)” 三个学生信息,s1 和 s2 因属性相同被去重;
  • 关键说明:若不重写hashCode()和equals(),s1 和 s2 会因哈希值不同(默认基于地址计算)被视为不同对象,无法实现去重。

五、进阶要点:线程安全与性能优化

1. 线程安全问题

HashSet、LinkedHashSet、TreeSet均为线程不安全集合,在多线程并发添加、删除元素时,可能导致ConcurrentModificationException异常或集合结构损坏。

解决方案
  1. 使用Collections.synchronizedSet()包装

// 将HashSet包装为线程安全集合

Set<String> safeSet = Collections.synchronizedSet(new HashSet<>());
  1. JDK1.8 + 使用ConcurrentHashMap.newKeySet()

// 基于ConcurrentHashMap实现,线程安全且性能更优

Set<String> concurrentSet = ConcurrentHashMap.newKeySet();

该方案底层依赖ConcurrentHashMap,采用分段锁机制,并发性能优于synchronizedSet。

2. 性能优化建议

  • 合理设置 HashSet 初始容量:HashSet默认初始容量为 16,加载因子为 0.75(当元素个数达到容量的 75% 时触发扩容)。若已知元素大致数量,可预设初始容量(如new HashSet<>(100)),减少扩容次数;
  • 简化 TreeSet 排序逻辑:避免在compareTo()或Comparator中执行复杂业务逻辑,减少排序过程中的性能消耗;
  • 优先选择 HashSet:在无顺序和排序需求的场景下,HashSet的性能最优,应作为首选。

六、总结:Set 实现类选择指南

根据业务需求选择合适的 Set 实现类,是提升代码性能和可读性的关键。以下为常见场景的选择建议:

需求场景

推荐实现类

核心理由

仅去重,无顺序要求

HashSet

性能最优,适用大多数场景

去重 + 保留添加顺序

LinkedHashSet

哈希表保证性能,链表保证顺序

去重 + 排序(自然 / 自定义)

TreeSet

红黑树实现排序,功能匹配

多线程并发场景

ConcurrentHashMap.newKeySet()

线程安全,性能优异

掌握 Set 集合的核心是理解 “底层结构决定特性,特性匹配业务场景”—— 从哈希表到红黑树,从去重到排序,每一种实现都是 Java 对 “性能与功能” 平衡的设计体现。结合本文的原理解析与实战案例,相信你能在实际开发中灵活运用 Set 集合解决问题。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值