retainAll是破坏性操作,会修改原Set;对不可变Set需先复制再调用;addAll返回boolean而非新Set;removeAll实现差集a−b;Stream方式性能较差且不推荐滥用。

Java中用 retainAll 求交集,但别直接在原Set上操作
交集不是“算出来一个新Set”,而是让调用方Set只保留和另一个Set共有的元素。所以 retainAll 是破坏性操作——它会修改原Set。
常见错误是:把不可变Set(比如 Set.of() 或 Collections.unmodifiableSet())传给 retainAll,立刻抛 UnsupportedOperationException。
- 正确做法:先复制一份可变副本,再调用
retainAll - 示例:
Set<String> a = new HashSet<>(Arrays.asList("a", "b", "c")); Set<String> b = Set.of("b", "c", "d"); Set<String> intersection = new HashSet<>(a); // 复制 intersection.retainAll(b); // 现在 intersection 是 ["b", "c"] - 注意:如果a本身是不可变的,
new HashSet<>(a)这一步就必不可少
并集用 addAll 最简单,但要注意返回值不是新Set
addAll 只是把另一个Set所有元素加进当前Set,不返回新对象,也不去重(HashSet自己保证)。
容易踩的坑是误以为它像Stream那样“链式返回”——它返回的是 boolean(是否发生了新增),不是Set本身。
立即学习“Java免费学习笔记(深入)”;
- 想保留原Set不变?必须手动复制:
Set<String> union = new HashSet<>(a); union.addAll(b);
- 如果a是TreeSet,
addAll后仍保持排序;如果是LinkedHashSet,插入顺序也保留 - 性能上,
addAll对于大Set比循环加单个元素快得多(批量优化)
差集用 removeAll,但“谁减谁”必须看清楚
a.removeAll(b) 的意思是“从a里删掉所有在b里出现的元素”,结果是 a − b(a对b的差集),不是对称差。
这个方向性非常容易搞反,尤其当变量名不够直观时(比如叫 set1、set2)。
- 典型错误现象:执行后得到空Set,其实只是把本该保留的元素全删了
- 安全写法:明确注释或封装成方法,例如
static <T> Set<T> difference(Set<T> from, Set<T> to) { Set<T> result = new HashSet<>(from); result.removeAll(to); return result; } - 注意:如果to里有null,而from不允许null(如TreeSet且没指定Comparator),会抛
NullPointerException
Stream方式更函数式,但别为了“看起来高级”牺牲可读和性能
Java 8+ 可以用 stream().filter().collect() 实现三类运算,但它不是银弹。
实际项目里,用Stream求交集/差集往往比原生集合操作慢2–5倍(尤其小集合),而且代码更长、调试更难。
- 仅当已有Stream流水线、或需要复合条件过滤时,才考虑Stream方式
- 例如差集带额外判断:
a.stream().filter(x -> !b.contains(x) && x.length() > 2).collect(Collectors.toSet())
- 不要用
Collectors.toSet()得到的Set做后续修改——它是未指定实现类,可能不可变
equals 和 hashCode 行为。如果元素是自定义对象,没重写这两个方法,交并差的结果几乎一定不对。










