哈喽,各位 Java 学习者!欢迎来到《Java 学习日记》的第十四篇内容~ 上一篇我们掌握了有序可重复的 List 接口,今天要学习它的 "互补型" 集合 ------Set 接口,以及它的两个核心实现类:HashSet(哈希去重)和TreeSet(排序去重)。Set 作为 "无序、不可重复" 的集合,是开发中实现 "去重" 和 "有序去重" 的核心工具。本文会从 Set 的核心特性、HashSet 的哈希去重原理、TreeSet 的排序机制,到实战场景选择,帮你彻底掌握 Set 的使用!
一、Set 接口:无序、不可重复的集合核心
1. 为什么需要 Set?
List 能存储重复元素,但开发中经常需要 "去重" 场景(如统计用户访问的唯一 IP、筛选不重复的商品 ID),Set 的核心价值就是保证元素唯一性,同时不维护插入顺序(HashSet)或按指定规则排序(TreeSet)。
2. Set 接口的核心特性
| 特性 | 说明 |
|---|---|
| 不可重复性 | 不允许存储重复元素(判断标准:equals()返回 true 且hashCode()值相等) |
| 无序性 | (HashSet)元素不按插入顺序排列,底层由哈希表决定;(TreeSet)按指定规则排序 |
| 无索引 | 不支持索引访问(无get(int index)方法),只能遍历 |
| 允许 null | (HashSet)允许存储一个 null;(TreeSet)不允许 null(排序时会空指针) |
3. Set 接口的常用方法(必背)
Set 继承自Collection接口,无新增方法,核心方法如下(与 List 相比,无索引相关方法):
| 方法 | 作用 | 示例 |
|---|---|---|
add(E e) |
添加元素(重复则返回 false,不添加) | set.add("Java") |
remove(Object o) |
删除指定元素(存在则返回 true) | set.remove("Java") |
contains(Object o) |
判断是否包含指定元素 | set.contains("Java") |
size() |
获取元素个数 | set.size() |
isEmpty() |
判断是否为空 | set.isEmpty() |
clear() |
清空所有元素 | set.clear() |
iterator() |
获取迭代器,用于遍历 | Iterator<String> it = set.iterator() |
实战 1:Set 接口基础用法(去重)
java
运行
java
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SetBasicDemo {
public static void main(String[] args) {
// 1. 创建Set对象(多态:父接口引用指向子类实现)
Set<String> set = new HashSet<>();
// 2. 添加元素(自动去重)
set.add("Java");
set.add("Python");
set.add("Java"); // 重复元素,添加失败
set.add(null); // HashSet允许一个null
System.out.println("添加重复元素后大小:" + set.size()); // 3(Java、Python、null)
// 3. 遍历元素(三种方式)
// 方式1:增强for循环(唯一推荐的遍历方式)
System.out.println("\n增强for循环遍历:");
for (String s : set) {
System.out.println(s); // 输出顺序不确定(无序)
}
// 方式2:迭代器遍历(支持删除元素)
System.out.println("\n迭代器遍历(删除null):");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
String s = it.next();
if (s == null) {
it.remove(); // 安全删除
} else {
System.out.println(s);
}
}
// 方式3:forEach Lambda(Java 8+)
System.out.println("\nforEach遍历:");
set.forEach(s -> System.out.println(s));
// 4. 其他常用方法
System.out.println("\n是否包含Python:" + set.contains("Python")); // true
set.remove("Python");
System.out.println("删除Python后大小:" + set.size()); // 1
}
}
二、HashSet:基于哈希表的去重集合(开发首选)
1. 底层原理
HashSet是 Set 接口的哈希表实现 ,底层依赖HashMap(Java 8+),核心逻辑是:
- HashSet 内部维护一个 HashMap,将元素作为 HashMap 的key存储(value 固定为一个空对象);
- 利用 HashMap 的 key 唯一性实现 Set 的去重;
- 哈希表底层是 "数组 + 链表 + 红黑树"(Java 8+),保证添加 / 查询 / 删除的高效性。
核心源码简化如下:
java
运行
java
public class HashSet<E> extends AbstractSet<E> implements Set<E> {
private transient HashMap<E, Object> map;
// 固定value,节省内存
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>(); // 底层创建HashMap
}
// 添加元素:本质是向HashMap中put(key, PRESENT)
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
// 删除元素:本质是删除HashMap的key
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
}
2. 去重核心规则(面试高频)
HashSet 判断元素是否重复的两步规则:
- 先比较哈希值 (
hashCode()方法):如果哈希值不同,直接判定为不同元素; - 若哈希值相同,再比较内容 (
equals()方法):equals()返回 true → 重复元素,不添加;equals()返回 false → 哈希冲突,存储到链表 / 红黑树中。
实战 2:自定义对象去重(必须重写 hashCode 和 equals)
java
运行
java
import java.util.HashSet;
import java.util.Set;
// 学生类:需重写hashCode和equals才能实现去重
class Student {
private String id; // 学号(唯一标识)
private String name;
private int age;
public Student(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// 重写equals:按学号判断是否为同一个学生
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id.equals(student.id); // 仅学号相同即为同一学生
}
// 重写hashCode:基于学号生成哈希值(保证equals相等则hashCode相等)
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return "Student{id='" + id + "', name='" + name + "', age=" + age + "}";
}
}
public class HashSetCustomObjDemo {
public static void main(String[] args) {
Set<Student> studentSet = new HashSet<>();
// 添加两个学号相同的学生(应视为重复)
studentSet.add(new Student("001", "张三", 18));
studentSet.add(new Student("001", "张三", 19)); // 重复,添加失败
studentSet.add(new Student("002", "李四", 20));
// 输出:仅2个元素(去重成功)
System.out.println("Set大小:" + studentSet.size()); // 2
studentSet.forEach(System.out::println);
}
}
3. 核心特性与性能分析
| 特性 | 说明 |
|---|---|
| 底层结构 | HashMap(数组 + 链表 + 红黑树) |
| 去重规则 | hashCode() + equals() |
| 顺序 | 无序(不保证插入顺序,也不保证排序) |
| 允许 null | 是(仅一个) |
| 线程安全 | 非线程安全(多线程需用Collections.synchronizedSet) |
| 时间复杂度 | 添加 / 查询 / 删除:O (1)(无哈希冲突),O (n)(极端哈希冲突) |
实战 3:HashSet 性能测试(去重效率)
java
运行
java
import java.util.HashSet;
import java.util.Set;
public class HashSetPerformanceDemo {
public static void main(String[] args) {
// 测试100万条数据去重
Set<Integer> set = new HashSet<>();
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
set.add(i % 100000); // 模拟10倍重复数据
}
long end = System.currentTimeMillis();
System.out.println("HashSet添加100万条重复数据耗时:" + (end - start) + "ms"); // 约20ms
System.out.println("去重后元素个数:" + set.size()); // 100000
}
}
三、TreeSet:基于红黑树的排序去重集合
1. 底层原理
TreeSet是 Set 接口的红黑树实现 ,底层依赖TreeMap,核心逻辑是:
- TreeSet 内部维护一个 TreeMap,将元素作为 TreeMap 的 key 存储;
- 利用红黑树的特性,自动对元素进行自然排序 (如数字升序、字符串字典序)或自定义排序;
- 排序的前提是元素实现
Comparable接口,或创建 TreeSet 时指定Comparator比较器。
核心源码简化如下:
java
运行
java
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E> {
private transient NavigableMap<E, Object> m;
private static final Object PRESENT = new Object();
public TreeSet() {
m = new TreeMap<>(); // 底层创建TreeMap
}
public TreeSet(Comparator<? super E> comparator) {
m = new TreeMap<>(comparator); // 指定自定义比较器
}
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
}
2. 排序核心规则(面试高频)
TreeSet 的排序分为两种方式,优先级:自定义比较器 > 自然排序:
方式 1:自然排序(元素实现 Comparable 接口)
- 元素需实现
Comparable接口,重写compareTo(T o)方法; compareTo返回值规则:- 返回 0 → 元素重复,不添加;
- 返回正数 → 当前元素大于参数元素;
- 返回负数 → 当前元素小于参数元素。
方式 2:自定义比较器(Comparator)
- 创建 TreeSet 时传入
Comparator对象,重写compare(T o1, T o2)方法; - 无需修改元素类,灵活性更高。
3. 核心特性与性能分析
| 特性 | 说明 |
|---|---|
| 底层结构 | TreeMap(红黑树) |
| 去重规则 | compareTo/compare 返回 0 即为重复 |
| 顺序 | 有序(自然排序 / 自定义排序) |
| 允许 null | 否(排序时会抛出 NullPointerException) |
| 线程安全 | 非线程安全 |
| 时间复杂度 | 添加 / 查询 / 删除:O (log n)(红黑树的平衡特性) |
实战 4:自然排序(实现 Comparable)
java
运行
java
import java.util.Set;
import java.util.TreeSet;
// 商品类:实现Comparable,按价格升序排序
class Goods implements Comparable<Goods> {
private String id;
private String name;
private double price;
public Goods(String id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// 自然排序:按价格升序,价格相同则按ID排序
@Override
public int compareTo(Goods o) {
// 先按价格比较
if (this.price > o.price) return 1;
if (this.price < o.price) return -1;
// 价格相同,按ID字典序
return this.id.compareTo(o.id);
}
@Override
public String toString() {
return "Goods{id='" + id + "', name='" + name + "', price=" + price + "}";
}
}
public class TreeSetNaturalSortDemo {
public static void main(String[] args) {
Set<Goods> goodsSet = new TreeSet<>();
// 添加商品(自动按价格升序排序)
goodsSet.add(new Goods("003", "手机", 2999.99));
goodsSet.add(new Goods("001", "电脑", 5999.99));
goodsSet.add(new Goods("002", "平板", 2999.99)); // 价格相同,按ID排序
// 遍历:按价格升序输出
System.out.println("按价格升序排序:");
goodsSet.forEach(System.out::println);
}
}
实战 5:自定义比较器(Comparator)
java
运行
java
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class TreeSetCustomComparatorDemo {
public static void main(String[] args) {
// 自定义比较器:按字符串长度降序,长度相同按字典序升序
Set<String> set = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
// 先按长度降序
int lenCompare = Integer.compare(s2.length(), s1.length());
if (lenCompare != 0) {
return lenCompare;
}
// 长度相同,按字典序升序
return s1.compareTo(s2);
}
});
// 添加元素(自动按自定义规则排序)
set.add("Java");
set.add("Python");
set.add("C");
set.add("Go");
set.add("JavaScript");
// 遍历:按长度降序输出
System.out.println("按字符串长度降序排序:");
set.forEach(System.out::println);
}
}
四、HashSet vs TreeSet:核心区别与场景选择
1. 核心区别对比表
| 特性 | HashSet | TreeSet |
|---|---|---|
| 底层结构 | 哈希表(HashMap) | 红黑树(TreeMap) |
| 排序性 | 无序 | 有序(自然排序 / 自定义排序) |
| 去重规则 | hashCode() + equals() | compareTo ()/compare () 返回 0 |
| 允许 null | 是(仅一个) | 否 |
| 性能 | 高(O (1)) | 中(O (log n)) |
| 元素要求 | 可选重写 hashCode/equals(自定义对象) | 必须实现 Comparable 或指定 Comparator |
| 常用方法 | 基础 Collection 方法 | 新增排序相关方法(如 first ()、last ()、ceiling ()) |
2. 场景选择最佳实践
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| 仅需去重,无需排序 | HashSet | 性能最高,实现简单 |
| 去重且需要排序(升序 / 降序 / 自定义规则) | TreeSet | 自动排序,无需手动处理 |
| 高频添加 / 查询 / 删除,无排序需求 | HashSet | O (1) 性能远超 TreeSet 的 O (log n) |
| 需要获取最大 / 最小元素 | TreeSet | 提供 first ()、last () 等便捷方法 |
| 自定义对象去重 | HashSet(重写 hashCode/equals) | 比 TreeSet 更简单,性能更高 |
实战 6:场景对比(去重 + 排序 vs 仅去重)
java
运行
java
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class SetScenarioDemo {
public static void main(String[] args) {
// 场景1:仅去重(用户访问的唯一IP)
String[] ips = {"192.168.1.1", "10.0.0.1", "192.168.1.1", "172.16.0.1"};
Set<String> ipSet = new HashSet<>();
for (String ip : ips) {
ipSet.add(ip);
}
System.out.println("唯一IP(无序):");
ipSet.forEach(System.out::println);
// 场景2:去重+排序(成绩排名)
Integer[] scores = {85, 90, 78, 90, 88, 95};
Set<Integer> scoreSet = new TreeSet<>();
for (Integer score : scores) {
scoreSet.add(score);
}
System.out.println("\n去重后成绩(升序):");
scoreSet.forEach(System.out::println);
}
}
五、Set 使用的高频坑点与优化技巧
1. 高频坑点
坑点 1:自定义对象去重失败(未重写 hashCode/equals)
- 问题 :HashSet 存储自定义对象时,未重写
hashCode()和equals(),导致重复元素无法去重; - 解决 :必须同时重写
hashCode()和equals(),且保证 "equals 相等的对象,hashCode 一定相等"。
坑点 2:TreeSet 添加 null 元素(空指针异常)
- 问题 :TreeSet 添加 null 会抛出
NullPointerException; - 解决:避免向 TreeSet 添加 null,或在比较器中处理 null。
坑点 3:TreeSet 排序规则冲突(compareTo 返回值错误)
- 问题 :
compareTo方法返回值逻辑错误,导致排序混乱或重复元素; - 解决 :严格遵循
compareTo返回值规则(0 = 相等,正数 = 大于,负数 = 小于)。
2. 优化技巧
技巧 1:HashSet 初始化指定容量(减少扩容)
java
运行
java
// 预估元素个数,指定初始容量和加载因子(默认0.75)
Set<String> set = new HashSet<>(100, 0.75f);
技巧 2:批量添加元素(addAll)
java
运行
java
// 批量添加比循环单个add更快(减少哈希冲突检查次数)
Set<String> set = new HashSet<>();
Set<String> data = new HashSet<>();
data.add("A");
data.add("B");
set.addAll(data);
技巧 3:TreeSet 使用 Lambda 简化比较器(Java 8+)
java
运行
java
// Lambda替代匿名内部类,代码更简洁
Set<String> set = new TreeSet<>((s1, s2) -> {
// 按长度降序
int lenCompare = Integer.compare(s2.length(), s1.length());
return lenCompare != 0 ? lenCompare : s1.compareTo(s2);
});
六、综合实战:Set 实现投票系统(去重 + 排序)
java
运行
java
import java.util.*;
// 投票选项类
class VoteOption implements Comparable<VoteOption> {
private String optionId; // 选项ID
private String optionName; // 选项名称
private int voteCount; // 票数
public VoteOption(String optionId, String optionName) {
this.optionId = optionId;
this.optionName = optionName;
this.voteCount = 0;
}
// 投票(票数+1)
public void vote() {
this.voteCount++;
}
// 自然排序:按票数降序,票数相同按ID升序
@Override
public int compareTo(VoteOption o) {
int countCompare = Integer.compare(o.voteCount, this.voteCount);
return countCompare != 0 ? countCompare : this.optionId.compareTo(o.optionId);
}
@Override
public String toString() {
return "选项:" + optionName + "(票数:" + voteCount + ")";
}
// getter
public String getOptionId() { return optionId; }
}
// 投票系统
public class VoteSystem {
// 存储已投票用户ID(去重,防止重复投票)
private Set<String> votedUserIds = new HashSet<>();
// 存储投票选项(排序,按票数降序)
private Set<VoteOption> voteOptions = new TreeSet<>();
// 初始化投票选项
public VoteSystem() {
voteOptions.add(new VoteOption("001", "Java"));
voteOptions.add(new VoteOption("002", "Python"));
voteOptions.add(new VoteOption("003", "C++"));
}
// 投票方法(去重+计数)
public boolean vote(String userId, String optionId) {
// 1. 检查是否重复投票
if (votedUserIds.contains(userId)) {
System.out.println("用户" + userId + "已投票,禁止重复投票!");
return false;
}
// 2. 查找投票选项
VoteOption targetOption = null;
for (VoteOption option : voteOptions) {
if (option.getOptionId().equals(optionId)) {
targetOption = option;
break;
}
}
if (targetOption == null) {
System.out.println("投票选项不存在!");
return false;
}
// 3. 记录投票用户,更新票数
votedUserIds.add(userId);
targetOption.vote();
System.out.println("用户" + userId + "投票成功!");
return true;
}
// 展示投票结果(按票数降序)
public void showVoteResult() {
System.out.println("\n===== 投票结果(按票数降序) =====");
voteOptions.forEach(System.out::println);
System.out.println("总投票人数:" + votedUserIds.size());
}
public static void main(String[] args) {
VoteSystem voteSystem = new VoteSystem();
// 模拟投票(包含重复投票)
voteSystem.vote("U001", "001"); // 成功
voteSystem.vote("U002", "001"); // 成功
voteSystem.vote("U001", "002"); // 重复投票,失败
voteSystem.vote("U003", "002"); // 成功
voteSystem.vote("U004", "003"); // 成功
// 展示结果
voteSystem.showVoteResult();
}
}
总结
关键点回顾
- Set 核心特性:无序(HashSet)/ 有序(TreeSet)、不可重复,无索引,是去重的核心工具;
- HashSet :哈希表实现,去重规则是
hashCode()+equals(),性能高,适合仅需去重的场景; - TreeSet :红黑树实现,排序去重,规则是
compareTo()/Comparator,适合需排序的去重场景; - 核心坑点 :自定义对象去重需重写
hashCode()和equals(),TreeSet 不能添加 null; - 场景选择:优先用 HashSet(性能高),需排序时用 TreeSet。
Set 是 Java 集合框架中与 List 互补的核心类型,掌握 HashSet 的去重原理和 TreeSet 的排序机制,能高效解决开发中的去重、排序问题。下一篇【Day15】,我们会讲解 "集合框架(三):Map 接口(HashMap、TreeMap)核心用法",帮你掌握键值对存储的核心容器!如果今天的内容对你有帮助,欢迎点赞 + 收藏 + 关注,有任何问题都可以在评论区留言,咱们一起讨论~ 明天见!🚀