如何在 Java 中利用 Stream.distinct() 配合自定义 equals 实现对象流的去重
Stream.distinct()方法依赖对象的equals()和hashCode()方法实现去重。若未重写,默认比较内存地址,导致自定义对象无法去重。正确做法是使用IDE生成或Lombok注解重写这两个方法,确保判重逻辑与业务一致。若无法修改类源码,可用Collectors.toMap手动去重。注意equals()应避免依赖外部可变状态,且需遵守hashC
如何在 Ja va 中利用 Stream.distinct() 配合自定义 equals 实现对象流的去重

distinct() 为什么对自定义对象默认不生效
很多开发者第一次用 Stream.distinct() 处理自定义对象列表时,都会遇到一个尴尬的情况:代码执行了,但列表长度纹丝不动。问题出在哪?其实,distinct() 方法的去重能力,完全建立在对象的 equals() 和 hashCode() 方法之上。
如果你没有在自定义类中重写这两个方法,那么 Ja va 就会搬出 Object 类的默认实现。这个默认实现比较的是对象的内存地址。这意味着,即便两个对象的每个字段值都一模一样,只要它们是 new 出来的两个不同实例,在 distinct() 眼里,它们就是两个完全不同的个体。日志里看到的,自然就是一排“看起来相同”却无法被去重的对象。
要让 distinct() 乖乖工作,必须同时满足两个条件:
- 在目标类中,必须重写
equals(Object)和hashCode()方法。 equals()方法中定义的“相等”逻辑,必须和你业务上期望的“去重维度”完全一致。比如,你是想只根据id判断,还是需要结合name和email一起判断?
如何安全地重写 equals 和 hashCode
这里有个核心建议:别自己手写。手动实现这两个方法极易出错,比如漏掉某个关键字段、忘记处理 null 值,或者破坏 equals 的对称性、传递性契约。最稳妥的方式是借助 IDE 的自动生成功能。
以 IntelliJ IDEA 为例:在类文件内右键,选择 Generate -> equals() and hashCode(),然后在弹出的窗口中,只勾选那些真正用于判断对象相等的字段。切记,不要图省事全选,否则那些本不该参与判重的辅助字段(比如一个临时的状态标记)也会被纳入计算,这往往会导致意想不到的“误伤”。
立即学习“Ja va免费学习笔记(深入)”;
这里有几个关键细节需要特别注意:
- 如果项目使用了 Lombok,推荐使用
@EqualsAndHashCode(onlyExplicitlyIncluded = true)注解,并在需要参与判重的字段上额外添加@EqualsAndHashCode.Include。否则,Lombok 默认会包含所有非静态、非瞬态的字段,这同样可能引入非预期的判重逻辑。 - 当你的字段本身是集合(如
List)或另一个自定义对象时,务必确保这些内部对象也正确定义了它们自己的equals()方法。否则,外层的判等检查依然会失败。 - 必须遵守
hashCode()与equals()之间的契约:如果两个对象根据equals()判断是相等的,那么它们的hashCode()值必须相同。反之则不一定成立,哈希值相同不代表对象相等。
distinct() 的替代方案:当不能改实体类时
现实开发中,我们常会遇到“巧妇难为无米之炊”的情况:需要去重的对象来自第三方库(比如 org.springframework.http.HttpHeaders),你根本无法修改其源码去重写 equals()。这时候,就得绕开 distinct(),另辟蹊径了。
一个常用的替代方案是使用 collect() 配合 Collectors.toMap() 来手动实现去重。例如,我们希望根据用户的 id 进行去重,并保留首次出现的那个:
list.stream()
.collect(Collectors.toMap(
User::getId,
user -> user,
(u1, u2) -> u1 // 当键冲突时,保留第一个值
))
.values();
采用这种方法,有几点需要留心:
- 它的返回值是
Collection,而不是List,这意味着元素的原始顺序可能丢失。如果需要保持顺序,可以传入LinkedHashMap::new作为 Supplier。 - 如果作为去重依据的键(如
id)可能为null,toMap()会直接抛出NullPointerException。你需要提前用filter()过滤掉null键,或者使用更复杂的Collectors.collectingAndThen()进行包装处理。 - 从性能角度看,
distinct()在并行流中可以是短路操作,而toMap()通常需要全量收集数据到 Map 中,在处理海量数据时,内存占用会稍高一些。
distinct() 的实际使用陷阱
即便正确重写了 equals 和 hashCode,distinct() 的使用之路也并非一马平川。一个极易被忽视的陷阱是:distinct() 本身是无状态的中间操作,但它所依赖的 equals() 方法,如果其内部逻辑访问了外部可变状态(例如一个静态计数器、或者当前系统时间戳),那么去重结果将变得不可预测,甚至在多线程环境下引发线程安全问题。
除此之外,还有几个典型的“坑”值得警惕:
- 在
parallelStream()中使用distinct()时,其底层实现依赖于ConcurrentHashMap。这就要求你的hashCode()方法必须能产生分布均匀的哈希值,否则严重的哈希冲突会导致去重效率急剧下降,甚至在某些极端情况下影响去重结果的正确性。 - 如果实体类的判重字段是
BigDecimal或Double这类浮点数,直接使用它们默认的equals()进行精度比较可能会因为微小的精度差异而失败。更安全的做法是,在equals()逻辑中,将它们转换为String进行比较,或者使用compareTo() == 0。 - 记住,Stream 流水线是一次性的。一旦调用了终结操作(如
forEach或collect),这个流就被消费掉了。如果想再次进行去重操作,必须重新生成 Stream。
说到底,技术实现从来不是最困难的部分。真正的挑战在于,在动手编码之前,就必须清晰地定义出业务层面“什么叫重复”。这个核心语义必须首先被准确地固化在 equals() 方法中,而不是事后试图通过复杂的 filter 或 map 操作去勉强弥补。想清楚了这一点,后面的代码自然水到渠成。
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















