HashSet 是 Java 集合框架中一个非常重要的实现,它基于 HashMap,有以下特点:
- 不保证元素顺序:元素的存储顺序(遍历顺序)不等于插入顺序,也不等于任何固定的顺序。
- 不允许重复元素:如果尝试添加一个已存在的元素,添加操作会失败,
set保持不变。 - 允许 null 元素:可以添加一个
null元素。 - 非线程安全:在多线程环境下需要外部同步。
遍历 HashSet 主要有以下四种常用方法,我们将逐一介绍,并提供代码示例和优缺点分析。
准备工作:创建一个示例 HashSet
为了演示,我们先创建一个 HashSet 并添加一些元素。
import java.util.HashSet;
import java.util.Set;
public class HashSetIterationExample {
public static void main(String[] args) {
// 创建一个 HashSet 并添加一些字符串
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
fruits.add("Grape");
fruits.add(null); // HashSet 允许存一个 null
fruits.add("Apple"); // 重复元素,不会被添加
System.out.println("原始 HashSet: " + fruits);
}
}
使用增强 for 循环 (For-Each Loop)
这是最常用、最简洁、也最推荐的方式,它内部是使用迭代器实现的,但语法更简单。
语法:
for (String fruit : fruits) {
// 处理 fruit
}
完整示例:
System.out.println("\n--- 方法一:使用增强 for 循环 ---");
for (String fruit : fruits) {
System.out.println(fruit);
}
优点:
- 代码简洁易读:语法非常清晰,是遍历集合的首选。
- 避免手动管理迭代器:不容易出错。
缺点:
- 不能在遍历过程中修改集合:如果在循环中调用
fruits.remove(fruit),会抛出ConcurrentModificationException异常,如果需要删除元素,请使用方法三或方法四。
使用迭代器 (Iterator)
这是最传统、最安全的方式,也是增强 for 循环的底层实现,它提供了在遍历过程中安全删除元素的能力。
语法:
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
// 处理 fruit
}
完整示例:
System.out.println("\n--- 方法二:使用迭代器 ---");
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
优点:
- 安全:可以在遍历过程中安全地删除元素。必须使用迭代器自己的
remove()方法。 - 通用:所有
Collection接口的实现都支持。
缺点:
- 代码稍显冗长:相比增强 for 循环,需要多写几行代码。
重要:如何在遍历时安全删除元素?
System.out.println("\n--- 使用迭代器安全删除元素 ---");
// 删除 "Orange"
Iterator<String> removeIterator = fruits.iterator();
while (removeIterator.hasNext()) {
String fruit = removeIterator.next();
if ("Orange".equals(fruit)) {
removeIterator.remove(); // 正确!使用迭代器的 remove 方法
}
}
System.out.println("删除 'Orange' 后的 HashSet: " + fruits);
错误示范:
// for (String fruit : fruits) {
// if ("Apple".equals(fruit)) {
// fruits.remove(fruit); // 会抛出 ConcurrentModificationException
// }
// }
使用 Java 8+ 的 forEach 和 Lambda 表达式
这是现代 Java 中非常流行和函数式的方式,代码非常优雅。
语法:
fruits.forEach(fruit -> {
// 处理 fruit
});
完整示例:
System.out.println("\n--- 方法三:使用 forEach 和 Lambda ---");
fruits.forEach(fruit -> System.out.println(fruit));
如果操作很简单,甚至可以简化为方法引用:
System.out.println("\n--- 使用 forEach 和方法引用 ---");
fruits.forEach(System.out::println);
优点:
- 代码简洁、现代:非常适合函数式编程风格。
- 内部使用迭代器:底层也是迭代器,所以遍历时直接调用集合的
remove()方法同样会抛出ConcurrentModificationException,但它提供了另一种删除方式。
缺点:
- 外部修改的限制:和增强 for 循环一样,不能直接在 Lambda 表达式中调用集合的
remove()方法。
如何在遍历时安全删除(Lambda 方式)?
可以使用 removeIf 方法,这是专门为这个场景设计的。
System.out.println("\n--- 使用 removeIf 删除元素 ---");
// 删除所有以 "G" 开头的元素
fruits.removeIf(fruit -> fruit != null && fruit.startsWith("G"));
System.out.println("删除以 'G' 开头的元素后: " + fruits);
removeIf 方法内部会安全地使用迭代器,所以不会抛出异常。
使用并行流 (Parallel Stream)
对于非常大的 HashSet,可以利用多核 CPU 的优势进行并行遍历,以提高处理速度。
语法:
fruits.parallelStream().forEach(fruit -> {
// 处理 fruit (注意:处理顺序是不确定的)
});
完整示例:
System.out.println("\n--- 方法四:使用并行流 ---");
// 注意:并行流的处理顺序是不确定的,并且是并发的
fruits.parallelStream().forEach(System.out::println);
优点:
- 性能高:对于大数据集,可以显著提高处理速度。
缺点:
- 顺序不确定:元素的遍历顺序完全无法预测,因为它们被分配到不同的线程中处理。
- 线程安全:
forEach内部的操作不是线程安全的,可能会导致数据竞争或不可预期的结果。 - 不适合小数据集:对于少量数据,并行化的开销可能比带来的收益更大。
总结与对比
| 方法 | 语法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 增强 for 循环 | for (T item : set) |
最简洁、最易读 | 遍历中不能修改集合 | 日常遍历,首选 |
| 迭代器 | while (iterator.hasNext()) |
最安全,可安全删除 | 代码稍显冗长 | 需要在遍历中删除或添加元素时 |
| forEach + Lambda | set.forEach(item -> ...) |
现代、简洁,函数式风格 | 遍历中不能直接修改集合 | 现代Java开发,代码优雅 |
| 并行流 | set.parallelStream().forEach(...) |
性能高,利用多核 | 顺序不确定,有线程安全风险 | 处理大数据集且顺序不重要时 |
最佳实践建议
- 仅遍历,不修改:如果只是简单地读取集合中的每一个元素,优先使用增强 for 循环,它是最直观、最不容易出错的选择。
- 遍历中需要删除元素:必须使用迭代器,或者调用集合的
removeIf方法(Java 8+)。 - 追求代码的函数式风格:如果项目使用 Java 8 或更高版本,并且代码逻辑简单,使用
forEach+ Lambda 表达式会让代码看起来更现代。 - 处理海量数据:当
HashSet中的元素数量非常庞大,并且处理每个元素的开销也比较大时,可以考虑使用并行流来提升性能,但要特别注意其不确定性和线程安全问题。
