Set 去重与集合运算
学习目标
- 按是否需要顺序、排序和范围查询选择 Set 实现。
- 使用 add、contains、removeAll、retainAll 等 API 完成去重和集合运算。
- 理解 equals/hashCode、Comparator、null 和不可变 Set 的边界。
核心知识点
专业术语
- Set:不允许重复元素的集合接口。
- HashSet:依赖 hashCode/equals 去重,通常不承诺遍历顺序。
- LinkedHashSet:去重并保留插入顺序。
- TreeSet:按自然顺序或 Comparator 排序,支持范围和邻近查找。
- EnumSet:针对单个枚举类型的紧凑集合实现。
白话解释与边界
去重的依据不是对象地址,而是 equals 和 hashCode;自定义对象放入 HashSet 后,不要修改参与这两个方法的字段。因此要保留输入顺序用 LinkedHashSet,要排序或找大于等于某值的元素用 TreeSet。Set 不支持按索引读取,若业务需要位置语义应选 List。
常用用法
HashSet():构造空的哈希集合
HashSet() 创建一个初始为空的可变集合,它依赖元素的 hashCode 和 equals 去重,但不保证迭代顺序。
// 语义:HashSet 初始为空;加入 "java" 和 "sql" 后保留两个不同元素。
import java.util.HashSet;
import java.util.Set;
Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("sql");
System.out.println(tags.size());
// 输出:2Set.add:新增并判断是否重复
add 在元素尚不存在时写入并返回 true,重复元素不会再次加入。
// 语义:add 在元素尚不存在时写入并返回 true,重复元素不会再次加入。
import java.util.HashSet;
import java.util.Set;
public class SetAddDemo {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
System.out.println(set.add("java"));
// 输出:true
System.out.println(set.add("java"));
// 输出:false
}
}Set.contains:判断元素是否存在
contains 按集合的相等规则查询元素,HashSet 通常依赖 hashCode 和 equals。
// 语义:contains 按集合的相等规则查询元素,HashSet 通常依赖 hashCode 和 equals。
import java.util.HashSet;
import java.util.Set;
public class SetContainsDemo {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("java");
boolean found = set.contains("java");
System.out.println(found);
// 输出:true
}
}Set.remove:删除元素
remove 删除匹配元素并返回是否成功,目标不存在时集合保持不变。
// 语义:remove 删除匹配元素并返回是否成功,目标不存在时集合保持不变。
import java.util.HashSet;
import java.util.Set;
public class SetRemoveDemo {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("java");
boolean removed = set.remove("java");
System.out.println(removed + ", " + set);
// 输出:true, []
}
}LinkedHashSet:去重并保留插入顺序
LinkedHashSet 在 HashSet 的去重基础上维护插入顺序,适合去重后稳定展示;需要排序时改用 TreeSet。
// 语义:LinkedHashSet 在 HashSet 的去重基础上维护插入顺序,适合去重后稳定展示。
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<>();
set.add("b");
set.add("a");
set.add("b");
System.out.println(set);
// 输出:[b, a]
}
}Set.addAll:并集
addAll 把另一个集合的元素加入当前集合,重复元素自动忽略;这是原地操作,需要保留原集合时先复制。
// 语义:addAll 把另一个集合的元素加入当前集合,重复元素自动忽略。
import java.util.LinkedHashSet;
import java.util.Set;
public class SetUnionDemo {
public static void main(String[] args) {
Set<String> all = new LinkedHashSet<>(Set.of("java", "sql"));
all.addAll(Set.of("sql", "web"));
System.out.println(all);
// 输出:[java, sql, web]
}
}Set.retainAll:交集
retainAll 只保留当前集合和参数集合共有的元素,属于原地修改。
// 语义:retainAll 只保留当前集合和参数集合共有的元素,属于原地修改。
import java.util.LinkedHashSet;
import java.util.Set;
public class SetIntersectionDemo {
public static void main(String[] args) {
Set<String> common = new LinkedHashSet<>(Set.of("java", "sql", "web"));
common.retainAll(Set.of("java", "web"));
// 关键变化:common.retainAll(Set.of("java", "web"));;只保留 java 和 web,删除 sql,返回 true;common 变为 [java, web]。
System.out.println(common);
// 输出:[java, web]
}
}Set.removeAll:差集
removeAll 删除当前集合中出现在参数集合里的元素;参数集合很大时可考虑它的查找复杂度。
// 语义:removeAll 删除当前集合中出现在参数集合里的元素。
import java.util.LinkedHashSet;
import java.util.Set;
public class SetDifferenceDemo {
public static void main(String[] args) {
Set<String> onlyLeft = new LinkedHashSet<>(Set.of("java", "sql", "web"));
onlyLeft.removeAll(Set.of("sql"));
// 关键变化:onlyLeft.removeAll(Set.of("sql"));;删除 onlyLeft 中的 sql,返回 true;onlyLeft 变为 [java, web]。
System.out.println(onlyLeft);
// 输出:[java, web]
}
}Set.size:统计元素数量
size 返回去重后的元素个数,而不是 add 的调用次数。
// 语义:size 返回去重后的元素个数,而不是 add 的调用次数。
import java.util.HashSet;
import java.util.Set;
public class SetSizeDemo {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(1);
System.out.println(set.size());
// 输出:1
}
}Set.isEmpty:判断集合是否为空
isEmpty 直接表达“没有元素”,比比较 size 是否为 0 更清晰。
// 语义:isEmpty 直接表达“没有元素”,比比较 size 是否为 0 更清晰。
import java.util.HashSet;
import java.util.Set;
public class SetIsEmptyDemo {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
System.out.println(set.isEmpty());
// 输出:true
set.add(1);
System.out.println(set.isEmpty());
// 输出:false
}
}Set.clear:清空集合
clear 删除当前集合的全部元素,但其他指向同一集合的引用仍指向它。
// 语义:clear 删除当前集合的全部元素,但其他指向同一集合的引用仍指向它。
import java.util.HashSet;
import java.util.Set;
public class SetClearDemo {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>(Set.of(1, 2));
set.clear();
// 关键变化:set.clear();;清空 set,无返回值;集合内容变为空。
System.out.println(set);
// 输出:[]
}
}Set.copyOf:创建不可修改快照
Set.copyOf 复制当前元素结构并拒绝 null;结果不能 add、remove,元素本身如果可变仍不自动深复制。
// 语义:Set.copyOf 复制当前元素结构并拒绝 null。
import java.util.HashSet;
import java.util.Set;
public class SetCopyOfDemo {
public static void main(String[] args) {
Set<String> source = new HashSet<>(Set.of("java"));
Set<String> snapshot = Set.copyOf(source);
source.add("sql");
System.out.println(snapshot);
// 输出:[java]
}
}TreeSet:自动排序
TreeSet 依靠自然顺序或 Comparator 排列元素,并用比较结果判断重复。
// 语义:TreeSet 依靠自然顺序或 Comparator 排列元素,并用比较结果判断重复。
import java.util.TreeSet;
public class TreeSetOrderDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>();
set.add(30);
set.add(10);
set.add(20);
System.out.println(set);
// 输出:[10, 20, 30]
}
}TreeSet.first:读取最小元素
first 返回排序后的第一个元素,空集合调用会抛 NoSuchElementException。
// 语义:first 返回排序后的第一个元素,空集合调用会抛 NoSuchElementException。
import java.util.TreeSet;
public class TreeSetFirstDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>();
set.add(30);
set.add(10);
System.out.println(set.first());
// 输出:10
}
}TreeSet.last:读取最大元素
last 返回排序后的最后一个元素,调用前要确认集合不为空。
// 语义:last 返回排序后的最后一个元素,调用前要确认集合不为空。
import java.util.TreeSet;
public class TreeSetLastDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>();
set.add(10);
set.add(30);
System.out.println(set.last());
// 输出:30
}
}TreeSet.ceiling:查询大于等于目标的最小元素
ceiling 返回不小于目标的最近元素,不存在时返回 null。
// 语义:ceiling 返回不小于目标的最近元素,不存在时返回 null。
import java.util.TreeSet;
public class TreeSetCeilingDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>(java.util.Set.of(10, 20, 30));
System.out.println(set.ceiling(15));
// 输出:20
System.out.println(set.ceiling(31));
// 输出:null
}
}TreeSet.floor:查询小于等于目标的最大元素
floor 返回不大于目标的最近元素,不存在时返回 null。
// 语义:floor 返回不大于目标的最近元素,不存在时返回 null。
import java.util.TreeSet;
public class TreeSetFloorDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>(java.util.Set.of(10, 20, 30));
System.out.println(set.floor(15));
// 输出:10
System.out.println(set.floor(9));
// 输出:null
}
}不常用但需要知道
TreeSet.lower:查询严格小于目标的最大元素
lower 排除等于目标的元素,较小元素不存在时返回 null。
import java.util.TreeSet;
public class TreeSetLowerDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>(java.util.Set.of(10, 20, 30));
System.out.println(set.lower(20));
// 输出:10
}
}TreeSet.higher:查询严格大于目标的最小元素
higher 排除等于目标的元素,较大元素不存在时返回 null。
import java.util.TreeSet;
public class TreeSetHigherDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>(java.util.Set.of(10, 20, 30));
System.out.println(set.higher(20));
// 输出:30
}
}TreeSet.subSet:查询区间视图
subSet 返回指定上下界之间的动态视图,需要独立结果时再复制。
import java.util.TreeSet;
public class TreeSetRangeDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>(java.util.Set.of(1, 2, 3, 4));
System.out.println(set.subSet(2, true, 4, false));
// 输出:[2, 3]
}
}TreeSet.headSet:查询小于上界的视图
headSet 返回上界之前的动态视图,布尔参数决定是否包含上界。
import java.util.TreeSet;
public class TreeSetHeadSetDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>(java.util.Set.of(1, 2, 3, 4));
System.out.println(set.headSet(3, true));
// 输出:[1, 2, 3]
}
}TreeSet.tailSet:查询大于下界的视图
tailSet 返回下界之后的动态视图,布尔参数决定是否包含下界。
import java.util.TreeSet;
public class TreeSetTailSetDemo {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>(java.util.Set.of(1, 2, 3, 4));
System.out.println(set.tailSet(3, false));
// 输出:[4]
}
}EnumSet.of:枚举状态集合
EnumSet 只能保存同一种枚举,适合权限或标志集合;它通常比 HashSet 更紧凑。
import java.util.EnumSet;
public class EnumSetDemo {
enum Permission { READ, WRITE, DELETE }
public static void main(String[] args) {
EnumSet<Permission> set = EnumSet.of(Permission.READ, Permission.WRITE);
System.out.println(set.contains(Permission.WRITE));
// 输出:true
}
}EnumSet.complementOf:枚举补集
补集只在同一个枚举类型内有意义;空集合需要用 EnumSet.noneOf(Permission.class) 提供类型。
import java.util.EnumSet;
public class EnumSetComplementDemo {
enum Permission { READ, WRITE, DELETE }
public static void main(String[] args) {
EnumSet<Permission> missing =
EnumSet.complementOf(EnumSet.of(Permission.READ));
// 关键变化:EnumSet.complementOf(EnumSet.of(Permission.READ));;按 Permission.READ 调用 of 创建值,返回新实例。
System.out.println(missing);
// 输出:[WRITE, DELETE]
}
}Collections.disjoint:判断两个集合是否没有交集
disjoint 只回答是否相交,不会生成交集;需要结果集合时使用 retainAll 的副本。
import java.util.Collections;
import java.util.Set;
public class DisjointSetDemo {
public static void main(String[] args) {
boolean disjoint = Collections.disjoint(Set.of("java"), Set.of("sql"));
System.out.println(disjoint);
// 输出:true
}
}用自定义 Comparator 构造 TreeSet
TreeSet 把 Comparator 的比较结果当作元素身份;本例中长度相同的 bb 和 cc 会被视为重复,比较器必须符合业务语义。
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetComparatorDemo {
public static void main(String[] args) {
TreeSet<String> set = new TreeSet<>(Comparator.comparingInt(String::length));
set.add("a");
set.add("bb");
set.add("cc");
System.out.println(set);
// 输出:[a, bb]
}
}简单案例
import java.util.LinkedHashSet;
import java.util.Set;
public class SetSummaryDemo {
public static void main(String[] args) {
Set<String> tags = new LinkedHashSet<>();
tags.add("java");
tags.add("java");
System.out.println(tags);
// 输出:[java]
}
}Set 的核心路径是去重与判断存在;需要顺序或排序时再选择对应实现。
易混点
- HashSet 不承诺顺序,LinkedHashSet 保留插入顺序,TreeSet 按比较结果排序。
- HashSet 依赖 equals/hashCode,TreeSet 依赖 compareTo/Comparator;两者可能对“相等”的定义不同。
- Set 没有按索引 get;需要位置访问时使用 List。
- TreeSet 的范围方法返回视图,Set.copyOf 返回不可修改快照,二者不要混用。
课后小问
为什么 TreeSet 中比较器返回 0 会让两个不同对象只保留一个? 答案:TreeSet 使用比较结果判断元素是否重复,返回 0 就认为排序意义上的同一元素。 解析:比较器必须与业务唯一性一致;如果只按姓名长度比较,长度相同的对象会互相覆盖。
Set.retainAll 与 Set.copyOf 的作用有什么不同? 答案:retainAll 做原地交集修改,copyOf 创建当前内容的不可修改副本。 解析:前者改变原 Set,后者隔离容器结构;它们都不会自动深复制可变元素。
本节小结
- HashSet 用于通用去重,LinkedHashSet 用于去重并保序,TreeSet 用于排序和范围查询。
- addAll、retainAll、removeAll 分别表达并集、交集和差集。
- EnumSet 适合枚举状态,Set.copyOf 适合不可修改快照。
- 自定义比较器决定 TreeSet 的排序和唯一性语义。
快速回顾
- 能按顺序、排序、范围查询需求选择 Set 实现。
- 能写出并集、交集和差集。
- 能解释 HashSet 与 TreeSet 的等价判断依据。
- 能识别 TreeSet 视图、Set.copyOf 快照和可变元素边界。
阿源的知识库