Java Stream distinct()默认依赖equals/hashCode,自定义对象需用Collectors.toMap按属性去重,或TreeSet+Comparator实现排序去重,封装通用工具方法更佳;不建议随意重写equals。

Java Stream 的 distinct() 默认按对象的 equals() 和 hashCode() 判断是否重复,对自定义对象直接调用会失效——因为默认比较的是引用地址。要按某个属性(比如 id、name)去重,得自己实现逻辑,不能只靠 distinct()。
用 Collectors.toMap 实现按属性去重
这是最常用、推荐的方式:把流转换为 Map,用目标属性作 key,自动覆盖重复 key 的值,再提取 values 即可保留“第一个”出现的对象。
- 代码简洁,语义清晰,支持任意属性(字符串、数字、枚举等)
- 保证结果顺序(使用
LinkedHashMap作为底层 map) - 适用于大多数场景,包括 null 属性安全处理(稍加判断即可)
示例:按 id 去重
List<User> uniqueUsers = users.stream()
.collect(Collectors.toMap(
User::getId, // key:去重依据
user -> user, // value:整个对象
(u1, u2) -> u1 // 冲突时保留第一个(u1)
))
.values()
.stream()
.collect(Collectors.toList());
用 TreeSet + 自定义 Comparator 实现去重
适合需要排序 + 去重的场景。TreeSet 本身不存重复元素,配合按属性比较的 Comparator,能天然实现“按某字段唯一”。
立即学习“Java免费学习笔记(深入)”;
- 去重同时完成升序/降序排列
- 注意:Comparator 必须严格遵循
compare(a,b)==0当且仅当 a 和 b 在业务上“相等” - 性能略低于 toMap(O(n log n)),但内存更可控(无需中间 Map)
示例:按 name 去重并排序
Set<User> uniqueByName = users.stream()
.collect(Collectors.toCollection(
() -> new TreeSet<>((u1, u2) -> u1.getName().compareTo(u2.getName()))
));
封装成通用工具方法(支持泛型 + 属性函数)
避免每次写重复逻辑,可抽一个静态工具方法:
public static <T, K> List<T> distinctBy(List<T> list, Function<T, K> keyExtractor) {
return list.stream()
.collect(Collectors.toMap(
keyExtractor,
t -> t,
(t1, t2) -> t1 // 保留先出现的
))
.values()
.stream()
.collect(Collectors.toList());
}
调用时非常干净:
List<User> byId = distinctBy(users, User::getId); List<User> byName = distinctBy(users, User::getName);
为什么不直接重写 equals/hashCode?
看似简单,但有明显缺点:
- 破坏对象语义:User 的“相等”本应由全部业务字段决定,只为 stream 临时去重而改全局逻辑不合理
- 影响其他模块:比如放进 HashSet、作为 Map key、序列化对比等都会意外受影响
- 无法灵活切换去重维度(有时按 id,有时按 name,有时组合)
所以——除非该对象在所有业务中都**永远只按某一个字段判定相等**,否则不要动 equals。


















