Stream.distinct()未去重主因是自定义类未同时重写equals()和hashCode();String等包装类可直接使用;按字段去重需用toMap();parallelStream().distinct()线程安全但不保序。

Stream.distinct() 为什么没去重?先看 equals 和 hashCode 是否配套
直接调用 Stream.distinct() 却发现重复对象没被去掉,大概率是自定义类没重写 equals() 和 hashCode(),或者只重写了其中一个。Java 的 distinct() 内部用的是 LinkedHashSet,它依赖 hashCode() 快速分桶、再用 equals() 精确比对。两者不一致会导致“看起来相等但被当成不同对象”。
实操建议:
- 必须同时重写
equals(Object)和hashCode(),且逻辑自洽(比如都基于id和name字段) - 用 IDE 自动生成(如 IntelliJ 的
Alt+Insert → equals and hashCode),别手写漏字段 - 如果字段含
null,确保equals()中用Objects.equals(a, b)而非a.equals(b)
String、Integer 等包装类能直接用 distinct() 吗?
可以,而且开箱即用。因为 String、Integer、Long 等 JDK 自带类型,其 equals() 和 hashCode() 已正确定义(值语义)。例如:
List<String> list = Arrays.asList("a", "b", "a", "c");
list.stream().distinct().collect(Collectors.toList()); // ["a", "b", "c"]
注意点:
-
new String("a")和"a"在equals()下相等,所以会被去重 - 但
distinct()不改变原始顺序,输出顺序和首次出现位置一致 - 如果数据量极大(千万级),
distinct()的内存占用≈去重后集合大小,需留意堆内存
想按某个字段去重(比如只看 id),还能用 distinct() 吗?
不能直接用——distinct() 只认整个对象的 equals(),不支持“按字段投影去重”。强行改 equals() 只比 id,会污染该类在其他场景下的语义(比如本该按全部字段判断相等的地方出错)。
正确做法是用 Collectors.toMap() 或 Collectors.collectingAndThen() 模拟“按 key 去重”:
Map<Long, User> map = users.stream()
.collect(Collectors.toMap(
User::getId, // key: id
user -> user, // value: 整个对象
(u1, u2) -> u1 // 冲突时保留第一个
));
List<User> distinctByid = new ArrayList<>(map.values());
关键细节:
-
toMap的 merge 函数决定保留哪个:用(u1, u2) -> u1保首个,(u1, u2) -> u2保末个 - 如果
getId()可能为null,需先过滤或改用Collectors.groupingBy()+map.values().stream().map(list -> list.get(0)) - 这种写法比
distinct()多一次哈希表构建,但语义清晰、无副作用
parallelStream().distinct() 线程安全吗?结果顺序还保证吗?
线程安全,但不保证顺序。JDK 文档明确说明:distinct() 在并行流中仍能正确去重(底层用线程安全的 ConcurrentHashMap 替代 LinkedHashSet),但输出顺序是任意的——因为多个线程并发收集,无法维持原始 encounter order。
所以:
- 若你依赖顺序(比如“取每个重复组的第一个”),别用
parallelStream().distinct() - 若只是统计去重后数量,或后续还要排序,那并行版能提速
- 没有银弹:并行带来的开销(拆分/合并/同步)在小数据集(
hashCode() 实现是否真和 equals() 对齐——哪怕只漏一个字段,或者用了可变字段(比如把 lastModifiedTime 放进 hashCode),都会让 distinct() 行为不可预测。


















