stream.distinct()去重本质是“首次出现保留+哈希判等”,依赖对象的hashcode()和equals()方法,顺序流用linkedhashset保序,并行流用分段hashset+concurrenthashmap合并但不保序,可变对象修改判等字段会导致去重失效。

Stream.distinct() 的去重本质是“首次出现保留 + 哈希判等”,它不靠人工比对,而是依赖 Java 集合的哈希机制自动完成——核心是 HashSet(顺序流)或 ConcurrentHashMap(并行流),判断依据始终是对象的 hashCode() 和 equals()。
去重依赖两个关键契约
distinct 不自己定义“什么是重复”,而是把判断权交给对象自身:
- 先调用
hashCode()快速分桶:相同哈希值才进入下一步比较; - 再用
equals()精确确认:只有哈希值相同 且 equals 返回 true,才视为重复。
这意味着:
– 对 String、Integer 等 JDK 类型,开箱即用,因为它们已正确定义了这两个方法;
– 对自定义类(如 User、Order),若未重写 equals() 和 hashCode(),默认按内存地址比较,字段完全相同的两个实例仍会被当作不同元素。
底层用的是 LinkedHashSet 而非普通 HashSet
虽然很多资料说“基于 HashSet”,但实际在顺序流中,distinct() 内部使用的是 LinkedHashSet(或等效逻辑),原因很实在:
- 保证去重后元素保持原始首次出现的顺序;
- 插入时记录顺序,遍历时按插入序返回,不是随机散列序;
- 相比
TreeSet不需排序开销,相比纯HashSet多一份顺序保障。
源码层面,DistinctOps 的 accept() 方法会调用 set.add(t),而该 set 实例正是具备插入序特性的集合。
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
并行流下哈希逻辑有优化但不保序
当调用 parallelStream().distinct() 时,为避免多线程竞争同一哈希表,JDK 采用分段策略:
- 将流切分为多个子任务,每个子任务维护独立的本地
HashSet; - 各段分别去重后,再合并结果——合并阶段用
ConcurrentHashMap或全局HashSet做二次去重; - 最终结果仍是唯一元素,但顺序不再保证,因为各段处理完成时间不确定,合并无插入序约束。
所以:需要保序 → 用 stream().distinct();只求去重不关心顺序且数据量大 → 可考虑并行,但别依赖输出顺序。
可变对象会让哈希逻辑失效
如果一个对象在进入流后、被 distinct() 处理期间,其影响 hashCode() 或 equals() 的字段被修改(比如 user.setName("new")),就会破坏哈希一致性:
- 对象 A 初始哈希为 100,加入 set;
- 后续修改 name 导致哈希变为 200;
- 再次遇到 A 时,set 按新哈希找桶,找不到旧位置,误判为“新元素”,造成重复。
解决办法很简单:
– 尽量使用不可变类(如 record、final 字段 + 无 setter);
– 若必须可变,确保所有参与判等的字段在流处理全程不变更。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










