1. 引言
先从一个生活中的例子开始。想象你要办一场派对,需要拟定一份来宾名单。你发出邀请后发现,同一个人被列了两次(甚至三次——谁让他这么爱参加派对!)。如果你使用的是普通列表(List),就很容易出现这样的重复。但如果你有一个能自动阻止同一个来宾被添加两次的集合——生活就简单多了。
这时就轮到 Set(集合、集合论意义上的“集合”)登场了。
Set 是一种只存储唯一元素的集合。如果尝试添加已经存在的元素,它不会被添加(也不会伤害任何人感情)。
Set 接口:基本特性
在 Java 中,Set 是一个定义“无重复”集合行为的接口。它继承自 Collection 接口,因此支持添加(add)、删除(remove)、检查元素是否存在(contains)以及遍历等操作。
关键特点:
- Set 中不可能有两个相同的元素。
- 元素的存储顺序可能是任意的——取决于具体实现。
- 没有索引:无法像列表那样通过下标访问元素。
声明语法
Set<String> guests = new HashSet<>();
2. HashSet:快速、简单、无顺序
HashSet 是 Set 接口最常用的实现。它基于哈希表(类似 HashMap,但没有“键–值”对,只有唯一的值)。它的最大优点是:增、删、查都很快。
HashSet 如何工作?
可以把它想象成一个箱子。为了快速判断箱子里是否已经有“类似”的东西,每件物品都会得到一个“编号”——哈希码。当你向 HashSet 中添加元素时,首先会计算该元素的哈希码。如果之前没出现过,就把元素放进集合;如果这个哈希已经存在,就会再通过 equals() 做一次相等性检查。只有在对象确实相同的情况下,新的元素才不会被添加。
也就是说,HashSet 会自动保证唯一性:两个相同的对象不会同时存在。
有个有趣的点:如果你使用自己的类并想把它们放进 HashSet,就需要重写 equals() 和 hashCode() 方法。否则集合可能表现得不可预期——看起来相同的对象会被视为不同。
HashSet 的主要方法
Set<String> guests = new HashSet<>();
guests.add("Ivan");
guests.add("Maria");
guests.add("Peter");
guests.add("Ivan"); // 重复项!不会被添加。
System.out.println(guests); // [Ivan, Maria, Peter] — 顺序可能是任意的
guests.remove("Peter"); // 删除元素
System.out.println(guests.contains("Maria")); // true
System.out.println(guests.size()); // 2
我们来用代码试试
假设在我们的应用中,希望保存唯一的任务名称,避免出现两个同名任务:
import java.util.HashSet;
import java.util.Set;
public class UniqueTasksDemo {
public static void main(String[] args) {
Set<String> tasks = new HashSet<>();
tasks.add("完成 Java 作业");
tasks.add("撸猫");
tasks.add("完成 Java 作业"); // 重复项!
System.out.println("任务列表:");
for (String task : tasks) {
System.out.println("- " + task);
}
// 列表中只有两个任务,重复项不会被添加
}
}
3. TreeSet:顺序很重要!
有时我们不仅需要唯一性,还需要有序的元素集合。比如,希望看到来宾名字按字母顺序排列,而不是随机顺序。这时可以使用 TreeSet。
TreeSet 是 Set 的一种实现,它按自然顺序(升序)存储元素。它基于“红黑树”结构。
TreeSet 使用示例
import java.util.Set;
import java.util.TreeSet;
public class SortedGuestsDemo {
public static void main(String[] args) {
Set<String> guests = new TreeSet<>();
guests.add("Wolly");
guests.add("Alex");
guests.add("Kate");
guests.add("Alex"); // 重复项!
System.out.println("来宾(按字母顺序):");
for (String guest : guests) {
System.out.println("- " + guest);
}
// 输出:
// - Alex
// - Wolly
// - Kate
}
}
请注意:如果你添加了重复项,它不会出现在集合中。一切都如预期!
什么时候使用 TreeSet?
- 当需要有序的唯一元素集合时。
- 当需要快速查找,但对插入速度不那么敏感时(比 HashSet 略慢)。
- 如果元素是你自定义的类,则它们需要是“可比较的”(实现 Comparable 接口),或者你需要提供自己的 Comparator。
4. 实用要点
HashSet vs TreeSet:该如何选择?
| 指标 | HashSet | TreeSet |
|---|---|---|
| 存储顺序 | 不保证 | 按升序排序 |
| 操作速度 | 更快 (O(1)) | 较慢 (O(log n)) |
| 类型要求 | 任意(只需 equals()/hashCode()) | Comparable 或 Comparator |
| 典型场景 | 需要对唯一元素进行快速访问时 | 需要有序输出/遍历时 |
使用 Set 的一些特点
- 没有索引。 与 List 不同,Set 没有 get(int index) 方法。如果需要按下标访问——请使用 List。
- 没有重复。 如果你尝试添加已存在的元素,它不会被添加。方法 add 将返回 false。
- 不保证顺序(TreeSet 除外)。 在 HashSet 中,元素顺序每次运行都可能不同。如果需要保持插入顺序,请使用 LinkedHashSet。
- null 值。
- HashSet 允许存储一个 null 元素。
- TreeSet 在没有提供专用 Comparator 的情况下不允许添加 null,否则会抛出 NullPointerException。
5. Set 的常见任务
从列表中去重
假设我们有一个学生名单,其中有些名字出现了两次。需要只保留唯一的名字:
import java.util.*;
public class RemoveDuplicatesDemo {
public static void main(String[] args) {
List<String> students = Arrays.asList("安娜", "Ingvar", "安娜", "Maria", "Ingvar", "帕维尔");
Set<String> uniqueStudents = new HashSet<>(students);
System.out.println("唯一的学生: " + uniqueStudents);
// 顺序不保证!
}
}
如果需要有序的结果——请使用 TreeSet:
Set<String> sortedUniqueStudents = new TreeSet<>(students);
System.out.println("唯一的学生(按字母顺序): " + sortedUniqueStudents);
检查唯一性(例如,用户登录名)
Set<String> usedLogins = new HashSet<>();
usedLogins.add("student1");
usedLogins.add("java_lover");
String newLogin = "student1";
if (usedLogins.contains(newLogin)) {
System.out.println("该登录名已被占用!");
} else {
System.out.println("登录名可用!");
}
遍历集合中的元素
可以使用 for-each 循环进行遍历:
for (String name : uniqueStudents) {
System.out.println(name);
}
6. 使用 Set 的常见错误
错误 1:期望 HashSet 有确定的元素顺序。 很多初学者会惊讶:为什么集合中的元素以“奇怪”的顺序输出。这是正常的——HashSet 不保证顺序。如果需要保持插入顺序——使用 LinkedHashSet;如果需要排序——使用 TreeSet。
错误 2:尝试通过索引访问元素。 有时会写出类似 set.get(0) 的代码。这样是不行的:Set 不支持下标访问。需要下标访问——请使用 List。
错误 3:存放可变对象。 如果你存放的对象中,用于 equals()/hashCode() 的字段是可变的,那么在修改这些字段后,该元素可能会对集合“不可见”。请让元素保持不可变,或不要修改其标识性字段。
错误 4:期望重复元素会被添加。 同一个元素添加多次不会增加集合大小——重复项会被忽略,方法 add 将返回 false。
错误 5:使用基本类型。 诸如 Set<int> 之类的写法无法通过编译。请使用包装类型:Set<Integer>、Set<Double> 等等。
GO TO FULL VERSION