Java中List交集默认不保留重复项,因retainAll()等基于Set语义只判断存在性;需用HashBag(最小频次)或Map手撸实现频次交集,并明确业务定义。

Java里List交集默认不保留重复项,得换思路
Java原生的 retainAll() 或 Stream + filter() 求交集,本质是基于元素存在性判断,重复次数全丢光——比如 [a,a,b] 和 [a,b,b] 交集只返回 [a,b],不是你想要的 [a,b](各取最小频次)或 [a,a,b,b](各取最大频次)。这不是bug,是设计使然:Set语义主导了交集逻辑。
用Bag模拟带频次的交集:Apache Commons Collections的HashBag
Apache Commons Collections 的 Bag 接口专为计数设计,HashBag 是最常用实现。它把每个元素当“可堆叠物品”,记录出现次数,交集就变成“取两袋中同一物品的较小计数”。
实操建议:
- 添加依赖:
org.apache.commons:commons-collections4:4.4(注意是 v4,v3 不支持泛型且已停更) - 把两个
List分别转成HashBag:new HashBag(list1)、new HashBag(list2) - 调用
retainAll()—— 这里行为和 List 不同:它会修改当前 Bag,只保留与参数 Bag 共有的元素,且频次取两者中的较小值 - 再用
toArrayList()转回 List,得到带重复的交集结果
示例:list1 = ["a","a","b"],list2 = ["a","b","b"] → 交集 Bag 含 "a":1、"b":1 → 转 List 得 ["a","b"](最小频次交集)
立即学习“Java免费学习笔记(深入)”;
想保留“最大频次”交集?addAll() 不行,得手写合并逻辑
Bag.addAll() 是并集(频次相加),不是交集;retainAll() 只支持最小频次。要“取各自最大出现次数的交集”,比如 ["a","a"] ∩ ["a","a","a"] → ["a","a","a"],没有现成方法。
必须手动遍历一个 Bag 的唯一元素,对每个元素取 Math.max(bag1.getCount(e), bag2.getCount(e)),再往新 Bag 里 add 对应次数。
注意点:
- 别用
getCount()前不检查是否存在——对不存在的元素它返回 0,安全 - 避免在循环里反复调用
uniqueSet(),它每次新建 Set;先存一份引用 - 如果 List 很大,
HashBag构建本身有哈希开销,比纯 List 遍历略慢
替代方案:不用第三方库,用Map<T, Integer>手撸频次统计
如果项目禁用 Commons Collections,用 HashMap 统计频次完全可行,代码稍长但无额外依赖。
步骤:
- 写个工具方法,输入
List<T>,输出Map<T, Integer>(键=元素,值=出现次数) - 对两个 Map,取 key 交集(
keySet().retainAll()),再对每个 key 计算最小/最大频次 - 按频次生成结果 List:
Collections.nCopies(count, key)+flatMap或循环 add
兼容性更好,Java 8+ 原生支持;但要注意 null 元素——HashMap 允许一个 null key,而 HashBag 默认不允许,这点容易漏判。
真正麻烦的不是选 Bag 还是 Map,而是业务里“交集”的定义模糊:用户说“保留重复”,没说清是要最小频次、最大频次,还是按顺序匹配。定好规则再动手,比调通代码还关键。










