1. 操作 union(并集)
在 Java 中,用于表示集合的是接口 Set<T>。与列表(List)不同,集合保证元素唯一,通常也不关心顺序(除非使用特殊实现,例如 LinkedHashSet)。最常用的实现——HashSet 和 TreeSet。它们的核心目标是快速判断元素是否存在,并确保没有重复。
什么时候需要使用集合?
- 当你需要唯一性:例如,网站所有唯一访客的列表。
- 当你需要快速检查元素是否存在:HashSet 的 contains 通常是常数时间。
- 当你要执行典型的集合运算:并集、交集、差集。
Union 指合并两个及以上的集合:结果包含两个来源集合中的所有元素(不重复)。
实践示例
假设有两个学生集合,分别参加社团“机器人技术”和“编程”:
Set<String> robotics = Set.of("Anya", "Boris", "Vika");
Set<String> programming = Set.of("Vika", "Gleb", "Dasha");
我们需要得到至少参加一个社团的所有学生的集合。
通过 Stream API 的解法
最直接的方式是把两个流合并,并收集到 Set:
Set<String> all = Stream.concat(
robotics.stream(),
programming.stream()
).collect(Collectors.toSet());
System.out.println(all); // [Anya, Boris, Vika, Gleb, Dasha]
说明:
- Stream.concat 合并两个流。
- collect(Collectors.toSet()) 将元素收集为集合(自动去重)。
替代方案:超过两个集合
如果有三个及以上社团,使用 Stream.of 和 flatMap:
Set<String> math = Set.of("Zhenya", "Vika", "Boris");
Set<String> all = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
System.out.println(all); // [Anya, Boris, Vika, Gleb, Dasha, Zhenya]
为什么用 Set?
因为 Set 会自动去重。如果收集到 List,同样的名字会出现多次。
2. 操作 intersection(交集)
Intersection 指同时存在于两个集合中的元素。
实践示例
找出同时参加“机器人技术”和“编程”的学生:
Set<String> robotics = Set.of("Anya", "Boris", "Vika");
Set<String> programming = Set.of("Vika", "Gleb", "Dasha");
通过 Stream API 的解法:
Set<String> both = robotics.stream()
.filter(programming::contains)
.collect(Collectors.toSet());
System.out.println(both); // [Vika]
说明:
我们遍历“机器人技术”的成员,仅保留那些也在“编程”中的人。结果是一个包含两个社团共有名字的集合。
替代方式(不使用 Stream API)
可以使用内置方法 retainAll(会修改当前集合):
Set<String> intersection = new HashSet<>(robotics);
intersection.retainAll(programming);
System.out.println(intersection); // [Vika]
不过本讲聚焦 Stream API,因此强调流式处理。
3. 操作 difference(差集)
Difference 指第一个集合中存在、但第二个集合中不存在的元素。
实践示例
找出只参加“机器人技术”而不参加“编程”的学生:
Set<String> robotics = Set.of("Anya", "Boris", "Vika");
Set<String> programming = Set.of("Vika", "Gleb", "Dasha");
通过 Stream API 的解法:
Set<String> onlyRobotics = robotics.stream()
.filter(name -> !programming.contains(name))
.collect(Collectors.toSet());
System.out.println(onlyRobotics); // [Anya, Boris]
说明:
我们过滤“机器人技术”的成员,只保留那些不在“编程”中的人。
替代方式(不使用 Stream API)
Set<String> difference = new HashSet<>(robotics);
difference.removeAll(programming);
System.out.println(difference); // [Anya, Boris]
4. 实践任务:处理用户列表
任务 1:找出只参加一个社团的学生
需要知道谁只参加“机器人技术”或只参加“编程”,但不是同时参加。这就是对称差(xor 为集合的异或):
Set<String> onlyOne = Stream.concat(
robotics.stream().filter(name -> !programming.contains(name)),
programming.stream().filter(name -> !robotics.contains(name))
).collect(Collectors.toSet());
System.out.println(onlyOne); // [Anya, Boris, Gleb, Dasha]
任务 2:来自多个社团的所有唯一学生列表
Set<String> all = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
System.out.println(all); // [Anya, Boris, Vika, Gleb, Dasha, Zhenya]
任务 3:找出不参加任何社团的学生
假设我们有全班所有学生名单:
Set<String> allStudents = Set.of("Anya", "Boris", "Vika", "Gleb", "Dasha", "Zhenya", "Igor", "Katya");
需要找出谁没有参加任何社团:
Set<String> attendees = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
Set<String> notInAny = allStudents.stream()
.filter(name -> !attendees.contains(name))
.collect(Collectors.toSet());
System.out.println(notInAny); // [Igor, Katya]
5. 重要注意事项:equals、hashCode 与性能
为什么正确实现 equals 和 hashCode 很重要?
所有与集合(Set)相关的操作都依赖 equals 和 hashCode 的正确性。如果你存放的是自定义类的对象(例如 Student),就必须重写这些方法,否则比较将不正确。
示例:
class Student {
String name;
int age;
// 别忘了重写 equals 和 hashCode!
}
如果不这样做,即使两个学生的名字和年龄相同,对 Set 来说它们也会被视为不同的对象。
为什么优先使用 Set 而不是 List?
- Set 的 contains 操作很快(通常是常数时间)。
- 在 List 中查找元素是线性时间,这在大集合时可能成为瓶颈。
- 对于集合运算(union、intersection、difference),Set 更高效也更符合语义。
6. 进行集合运算时的常见错误
错误 1:用 List 代替 Set 来做集合运算。 如果你把元素收集到 List,则不会去重,而且 contains 操作很慢。对于 union/intersection/difference,请使用 Set。
错误 2:对象未实现 equals/hashCode。 如果你在 Set 中存放自定义类的对象,却没有重写 equals 和 hashCode,那么交集与差集会出现“奇怪”的结果——语义上相同的对象不会被视为相等。
错误 3:在流处理中修改集合。 如果你在流中直接修改原始 Set(例如添加或删除元素),会抛出 ConcurrentModificationException。请始终对新集合进行操作。
错误 4:不易察觉的顺序丢失。 HashSet 不保证元素的顺序。如果顺序很重要——请使用 LinkedHashSet 或 TreeSet。
错误 5:对两个以上的集合还用 Stream.concat。 Stream.concat 只能合并两个流。对于更多集合,请使用 Stream.of(...) 和 flatMap。
错误 6:关于 null 的问题。 集合通常不喜欢 null 值,尤其当你使用 Set.of(...) 时——它不允许 null。若需要处理 null,请使用其他实现,或提前过滤。
GO TO FULL VERSION