distinct()对自定义对象默认不生效,因其依赖equals()和hashCode()判断重复;未重写时使用Object默认实现(基于内存地址),导致字段相同但实例不同的对象无法去重。

distinct() 为什么对自定义对象默认不生效
Stream.distinct() 底层依赖对象的 equals() 和 hashCode() 判断是否重复。如果你没重写这两个方法,它用的是 Object 的默认实现——也就是基于内存地址比较。哪怕两个对象字段值一模一样,只要不是同一个实例,distinct() 就认为它们不同。
常见错误现象:stream.distinct() 调用后集合长度没变,日志里看到一堆“相同”对象还在。
必须同时满足两个条件才有效:
- 在目标类中重写
equals(Object)和hashCode() -
equals()的逻辑要和你期望的“去重维度”一致(比如只比id,或比name + email)
如何安全地重写 equals 和 hashCode
别手写。IDE 自动生成最稳妥,避免漏掉字段、null 判断或对称性问题。以 IntelliJ 为例:右键 → Generate → equals() and hashCode(),勾选你真正用于判重的字段(比如 userId 和 tenantId),不要全选。
立即学习“Java免费学习笔记(深入)”;
关键细节:
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
- 如果用 Lombok,加
@EqualsAndHashCode(onlyExplicitlyIncluded = true)+ 在字段上加@EqualsAndHashCode.Include,否则它会把所有非静态非瞬态字段都纳入,容易误伤 - 字段含
Collection或嵌套对象时,确保其自身也正确定义了equals(),否则外层判等会失败 -
hashCode()必须与equals()保持契约:相等的对象必须有相同哈希值;哈希值相同不要求一定相等
distinct() 的替代方案:当不能改实体类时
如果实体是第三方库的类、或你无权修改源码(比如 org.springframework.http.HttpHeaders),就不能靠重写 equals()。这时得绕过 distinct(),用 collect() + Collectors.toMap() 或 Collectors.toCollection() 手动控制去重逻辑。
例如按 id 去重(保留第一个):
list.stream()
.collect(Collectors.toMap(
User::getId,
user -> user,
(u1, u2) -> u1 // 保留第一个
))
.values();
注意点:
- 这个操作返回的是
Collection<User>,不是List,顺序不保证(除非用LinkedHashMap::new作 supplier) - 如果去重键可能为
null,toMap()会抛NullPointerException,需提前过滤或用Collectors.collectingAndThen()包装 - 性能上,
distinct()是短路操作,而toMap()是全量收集,大数据量时内存占用略高
distinct() 的实际使用陷阱
最容易被忽略的一点:distinct() 是无状态中间操作,但它依赖的 equals() 如果访问了外部可变状态(比如某个静态计数器、或当前时间戳),结果就不可预测,甚至线程不安全。
另一个典型坑:
- 在
parallelStream()中用distinct(),底层用的是ConcurrentHashMap,但前提是你的hashCode()分布足够均匀,否则会出现哈希冲突导致去重失效或性能骤降 - 如果实体字段是
BigDecimal或Double,直接用equals()比较可能因精度问题失败,建议统一转成String或用compareTo() == 0 - Stream 流水线一旦结束(比如调用了
forEach或collect),就不能再复用;反复调用distinct()要重新生成 stream
真正难的从来不是写几行代码,而是想清楚“按什么算重复”——这个业务语义必须先落在 equals 里,而不是靠后面补 filter 或 map 来硬凑。

















