HashSet去重最常用但不保序,需重写equals/hashCode;Stream.distinct()保序但性能略低;手动遍历效率差;业务去重需用toMap或groupingBy结合merge逻辑。

用 HashSet 去重是最常用也最直接的方式
Java 中最常用的去重方法是把集合转成 HashSet,利用其“不允许重复元素”的特性自动过滤。适用于不需要保持原顺序、且元素实现了 equals() 和 hashCode() 的场景。
常见错误现象:HashSet 去重后顺序完全打乱;自定义对象去重失败(比如始终认为所有对象都不同)。
- 确保自定义类重写了
equals()和hashCode(),否则HashSet无法正确判断相等性 - 如果需要保留插入顺序,改用
LinkedHashSet替代HashSet - 原始集合为
null时会抛NullPointerException,需提前判空
示例:
Listlist = Arrays.asList("a", "b", "a", "c");
SetuniqueSet = new HashSet<>(list); // 结果:[a, b, c],顺序不确定
用 Stream.distinct() 保持顺序并去重
JDK 8+ 提供的流式去重方式,底层依赖元素的 equals() 判断,天然保留原始顺序(前提是源集合有序,如 ArrayList)。
立即学习“Java免费学习笔记(深入)”;
使用场景:需要链式调用、配合其他流操作(如过滤、映射),或明确要求结果顺序与原集合一致。
-
distinct()是有状态操作,性能略低于纯遍历,大数据量时注意 GC 压力 - 对数组或基本类型数组不直接适用,需先转为包装类型流(如
Arrays.stream(ints).boxed().distinct()) - 自定义对象同样依赖
equals()/hashCode(),未重写则每个实例都被视为不同
示例:
Listnums = Arrays.asList(1, 2, 2, 3, 1);
Listunique = nums.stream().distinct().collect(Collectors.toList()); // [1, 2, 3]
手动遍历 + contains() 去重——可控但低效
显式循环判断是否已存在,适合需要额外逻辑(如去重时记录重复次数、跳过特定条件元素)的场景,但时间复杂度为 O(n²),仅建议用于小数据量或教学演示。
容易踩的坑:ArrayList.contains() 在大数据量下非常慢;误用 == 替代 equals() 导致对象比较失效。
- 若必须手动控制,优先用
HashSet辅助判断(即边遍历边往HashSet放,用其add()返回值判断是否新增) - 避免在循环中直接修改正在遍历的集合(如
remove()),会触发ConcurrentModificationException - 注意
null元素:多数集合允许一个null,但TreeSet等不支持
按业务规则去重(如只保留最新/最大值)不能靠基础集合类
HashSet 和 distinct() 只能做“存在性”去重,无法处理“保留某一个”的业务逻辑,比如相同 ID 的用户对象中只留更新时间最新的那个。
这时得结合 Collectors.toMap() 或分组聚合:
- 用
toMap(keyMapper, valueMapper, mergeFunction),其中mergeFunction决定冲突时留谁(如(old, now) -> now表示留新) - 用
Collectors.groupingBy()分组后再取每组的极值,适合需要统计或复杂筛选的场景 - 注意
toMap的 key 不能为null,否则抛NullPointerException
示例:
List实际去重时,真正麻烦的往往不是选哪个 API,而是没想清楚“重复”的定义——是字段全等?ID 相同?还是某个业务键一致?以及是否接受副作用(如修改原集合)、是否容忍users = ...;
MaplatestUserMap = users.stream()
.collect(Collectors.toMap(
User::getId,
Function.identity(),
(u1, u2) -> u1.getUpdateTime().isAfter(u2.getUpdateTime()) ? u1 : u2
));
null、是否要兼容 JDK 7。这些细节比语法本身更容易导致线上问题。










