
Stream.distinct() 对自定义对象无效?一定是 equals 和 hashCode 没配对
Java 的 Stream.distinct() 底层靠的是 Set 去重,而 Set 判断重复依赖 equals(和 hashCode)。如果你没重写这两个方法,它默认用的是 Object 的实现——也就是比较内存地址。哪怕两个对象字段一模一样,只要不是同一个实例,就视为不同。
- 常见错误现象:
distinct()后列表长度没变,或者只去掉了完全相同的引用(比如重复调用new Person("a", 1)两次) - 必须同时重写
equals和hashCode:只改equals不改hashCode,会导致哈希冲突,HashSet可能漏判相等 - IDE 生成时注意字段选择:只选参与“业务相等性判断”的字段(比如
id或name + age),别把createTime或临时缓存字段塞进去
用 Lombok 省事但容易踩坑:@EqualsAndHashCode 的 exclude 和 callSuper
Lombok 的 @EqualsAndHashCode 能自动生成,但默认行为可能不符合预期。比如父类字段要不要参与比较?某些字段是否该忽略?不显式声明,很容易出错。
- 常见错误现象:继承自某个框架实体类(如 JPA 的
BaseEntity),子类加了@EqualsAndHashCode却没设callSuper = true,导致父类的id被忽略 - 关键参数:
@EqualsAndHashCode(exclude = "tempField")排除临时字段;@EqualsAndHashCode(callSuper = true)显式包含父类逻辑 - 性能影响:排除大量字段可略微提升
hashCode计算速度,但一般可忽略;重点是语义正确——你认为“什么才算同一个对象”
替代方案:不用 distinct(),改用 collectingAndThen + toMap 按字段去重
当去重要求更复杂(比如按某个字段唯一、保留最新/最早的那个),distinct() 就不够用了。这时候用 Collectors.toMap 更可控,也更易读。
- 使用场景:按
id去重,但保留最后一次出现的对象(比如流里有更新数据) - 实操写法:
list.stream() .collect(Collectors.collectingAndThen( Collectors.toMap( Person::getId, Function.identity(), (oldVal, newVal) -> newVal // 冲突时取新值 ), map -> new ArrayList<>(map.values()) )); - 注意点:
toMap会抛NullPointerException如果 key 是null;如果字段可能为空,得先filter(Objects::nonNull)
调试技巧:在 equals 里加日志或断点,别猜
去重失败时,最有效的方法不是反复改 Stream 写法,而是确认 equals 是否真被调用了、传入的参数是否符合预期。尤其要注意 IDE 自动生成的 equals 是否包含了所有必要字段、是否做了空安全检查。
立即学习“Java免费学习笔记(深入)”;
- 容易被忽略的地方:字段类型是包装类(如
Integer)时,直接用==比较会出错,必须用Objects.equals(a, b) - 建议在
equals开头加一行日志(开发期):System.out.println("equals called: " + this + " vs " + obj);,看是不是压根没进这个方法 - 如果用了 Hibernate/JPA,注意代理对象问题:
Person$$EnhancerByCGLIB$$这种代理类可能导致getClass() != obj.getClass()为 true,从而跳过字段比较——这时得用obj instanceof Person替代类型强判








