JAVA重点基础、进阶知识及易错点总结(9)Set 接口(HashSet、LinkedHashSet、TreeSet)
·
🚀 Java 巩固进阶 · 第9天
主题:Set 接口深度解析 —— 去重与排序的艺术
📅 进度概览:掌握处理“唯一性”数据的终极武器。
💡 核心价值:
- 数据清洗:自动去除重复数据(如用户标签、权限列表、日志 ID)。
- 排序需求:
TreeSet提供天然的排序能力,无需手动Collections.sort。- 性能优化:理解哈希冲突与红黑树代价,避免在大数据量下误用导致性能下降。
- 面试高频:
hashCode与equals契约、HashSet 底层原理、TreeSet 排序陷阱。
一、Set 接口核心特性:唯一性的契约
Set 是 Collection 的子接口,核心特征是 「元素唯一」。
- 无索引:不支持
get(index),add(index, e),只能通过迭代器或增强 for 循环遍历。 - null 值:允许存一个
null(HashSet/LinkedHashSet),但TreeSet不允许 (无法比较 null)。 - 有序性定义:
- 无序:指不保证插入顺序 (HashSet)。
- 有序:指按插入顺序 (LinkedHashSet) 或 排序规则 (TreeSet)。
二、HashSet:高性能去重王者
1. 底层原理揭秘
- 本质:
HashSet是HashMap的包装器。 - 存储结构:
- 添加元素
e→ 实际调用map.put(e, PRESENT)。 PRESENT是一个静态的Object常量,Value 毫无意义,只利用 HashMap 的 Key 唯一性。
- 添加元素
- 去重逻辑:
- 计算
hashCode()确定桶位置。 - 若桶为空,直接存入。
- 若桶有值(哈希冲突),遍历链表/红黑树,调用
equals()比较。 - 若
equals()为 true,视为重复,覆盖 Value (还是 PRESENT) 并返回 false。
- 计算
2. ⚠️ 核心契约:hashCode() 与 equals()
自定义对象去重必须同时重写这两个方法!
- 契约:
- 若
a.equals(b) == true,则a.hashCode() == b.hashCode()必须成立。 - 若
hashCode()不同,equals()一定为 false。
- 若
- 后果:若只重写
equals未重写hashCode,两个逻辑相同的对象可能算出不同哈希值,存入不同桶,导致去重失败。
3. 🚀 现代写法:Lombok 简化
在 SpringBoot 项目中,我们极少手写 hashCode/equals,而是使用 Lombok。
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Objects;
@Data // 生成 getter, setter, toString, equals, hashCode
@EqualsAndHashCode(of = {"id", "name"}) // 指定仅根据 id 和 name 判断重复
public class User {
private Long id;
private String name;
private String email; // 不参与去重判断
}
(若不使用 Lombok,请使用 IDE 自动生成或 Objects.hash(id, name))
4. 💣 致命陷阱:可变对象作为 Key
严禁将可变字段(如 List, 可修改的 String Builder, 或会被修改的业务对象)作为 HashSet 的 Key,或者在放入 Set 后修改了影响 hashCode 的字段。
- 现象:元素“消失”了(
contains返回 false),但实际上还在集合里,只是找不到桶位置了,造成内存泄漏。
三、LinkedHashSet:有序的守护者
1. 原理
- 继承自
HashSet。 - 底层 =
HashMap+ 双向链表。 - 链表维护了元素的插入顺序。
2. 场景
- 缓存淘汰:结合访问顺序可实现 LRU (Least Recently Used) 缓存基础。
- 日志去重:保留第一次出现的时间顺序,去除后续重复日志。
- 配置加载:读取配置文件列表,去重但保持原有配置优先级顺序。
Set<String> set = new LinkedHashSet<>();
set.add("A"); set.add("B"); set.add("A");
// 遍历结果:A, B (严格保持插入顺序)
四、TreeSet:排序专家
1. 原理
- 底层 =
TreeMap(红黑树)。 - 元素存储在树节点上,天然有序。
- 复杂度:增删改查均为 O(log n),性能低于 HashSet (O(1))。
2. 排序两种方式
A. 自然排序 (Comparable)
类实现 Comparable 接口。
public class User implements Comparable<User> {
private Integer age;
// ...
@Override
public int compareTo(User o) {
return this.age.compareTo(o.getAge()); // 按年龄升序
}
}
B. 定制排序 (Comparator)
构造时传入比较器,优先级高于自然排序。
// 按年龄降序,年龄相同按 ID 升序
Set<User> set = new TreeSet<>((u1, u2) -> {
int cmp = u2.getAge().compareTo(u1.getAge()); // 降序
return cmp != 0 ? cmp : u1.getId().compareTo(u2.getId());
});
3. ⚠️ TreeSet 的特殊陷阱
- 去重依据:TreeSet 仅依赖
compareTo(或compare) 的返回值是否为 0 来判断重复,忽略equals()! - 风险:若排序规则认为两个对象相等 (返回 0),但
equals认为不等,后者将被丢弃。- 例子:只按“姓名”排序,两个同名不同 ID 的人,第二个会被误判为重复而丢弃。
- 解决:排序规则必须包含唯一标识(如 ID),确保逻辑唯一的对象不会被误删。
五、SpringBoot 中的去重策略对比
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 普通去重 (最快) | HashSet / Stream.distinct() |
O(1) 性能,最常用 |
| 保留顺序去重 | LinkedHashSet |
需维持原始次序 |
| 排序去重 | TreeSet / Stream.sorted().distinct() |
需结果有序 |
| 流式处理去重 | list.stream().distinct().collect(...) |
代码简洁,适合链式调用 |
| 并发环境去重 | ConcurrentSkipListSet |
线程安全的 TreeSet 替代品 |
💡 进阶提示:在 SpringBoot Service 层,如果只是为了临时去重,优先使用
Stream.distinct(),代码可读性更高;如果需要长期维护一个唯一的集合容器,则使用HashSet。
六、🎯 今日实战任务:用户标签管理系统
背景:管理用户的标签系统,需要支持快速去重、保留添加顺序、以及按热度排序。
需求步骤
- 定义
Tag类:- 属性:
id(Long),name(String),hotScore(Integer, 热度分)。 - 使用 Lombok (
@Data,@EqualsAndHashCode(of="id")) 简化代码。
- 属性:
- 功能验证:
- 场景 A (HashSet):创建
HashSet<Tag>,添加 5 个标签(其中 2 个 ID 重复)。验证大小是否为 4。 - 场景 B (LinkedHashSet):创建
LinkedHashSet<Tag>,添加相同数据。遍历时验证顺序是否与添加顺序一致。 - 场景 C (TreeSet):创建
TreeSet<Tag>,使用 Comparator 按hotScore降序排序(若分数相同按 ID 升序)。- 注意:确保 Comparator 中包含 ID 比较,防止同分不同 ID 的标签被误删。
- 场景 A (HashSet):创建
- 陷阱测试 (必做):
- 创建一个
Tag对象放入HashSet。 - 修改该对象的
id字段(破坏 hashCode)。 - 尝试
set.contains(tag)和set.remove(tag),观察是否还能找到/删除该元素?(预期:找不到,演示内存泄漏风险)。
- 创建一个
- Stream 替代方案:
- 有一个包含重复 Tag 的
List<Tag>。 - 使用
stream().distinct()去重,并收集回 List。 - 对比代码简洁度。
- 有一个包含重复 Tag 的
💡 代码提示
// TreeSet 定制排序 (防止同分误删)
Set<Tag> sortedTags = new TreeSet<>((t1, t2) -> {
int scoreCmp = t2.getHotScore().compareTo(t1.getHotScore()); // 热度降序
return scoreCmp != 0 ? scoreCmp : t1.getId().compareTo(t2.getId()); // ID 升序兜底
});
📝 第9天 · 核心总结
- Set 选型指南:
- 默认:
HashSet(最快,无序)。 - 要顺序:
LinkedHashSet(略慢,保序)。 - 要排序:
TreeSet(O(logn),需定义比较规则)。
- 默认:
- 去重铁律:
- 自定义对象必须重写
hashCode()+equals()。 - Lombok 是神器,但要清楚它生成的逻辑。
- TreeSet 特例:去重看
compareTo,务必保证排序规则的唯一性(包含主键)。
- 自定义对象必须重写
- 避坑指南:
- 禁止修改已放入 HashSet/TreeSet 对象的“关键比较字段”,否则会导致集合“失忆”(无法查找/删除)。
- 若对象不可变(Immutable),则是 Set 的最佳搭档。
- SpringBoot 实践:
- 权限列表 (
Set<String> roles) 必用 Set 去重。 - 流式数据处理优先
stream().distinct()。 - 高并发排序去重考虑
ConcurrentSkipListSet。
- 权限列表 (
更多推荐




所有评论(0)