distinct()严格依赖equals()和hashCode()的正确实现;自定义类必须重写二者,否则字段相同也被视为不同;按字段去重需filter+ConcurrentHashMap;去重逻辑须匹配业务语义。

Java 8 Stream.distinct() 的适用前提和限制
distinct() 方法本身不依赖元素的业务逻辑,但它**严格依赖 equals() 和 hashCode() 的正确实现**。如果自定义对象没重写这两个方法,哪怕两个对象字段完全相同,distinct() 也会把它们视为不同元素。
常见错误现象:Stream.of(new User("Alice", 25), new User("Alice", 25)).distinct().count() 返回 2 —— 因为默认使用 Object 的 hashCode(),地址不同即不同。
- 对
String、Integer等 JDK 内置类型可直接用distinct() - 对自定义类,必须确保
equals()/hashCode()覆盖了所有参与“相等判断”的字段 - 注意:
distinct()是有状态操作,不能并行流中随意替换为无状态替代方案
按对象某个字段去重(比如 name 去重保留第一个)
标准 distinct() 不支持“按字段去重”,它只认整个对象是否重复。要实现“按 name 去重”,本质是需要一个“已见 name 集合”做状态记录。
推荐写法(线程安全、语义清晰):
立即学习“Java免费学习笔记(深入)”;
Setseen = ConcurrentHashMap.newKeySet(); list.stream() .filter(user -> seen.add(user.getName())) .collect(Collectors.toList());
- 用
ConcurrentHashMap.newKeySet()而非HashSet,避免并行流下add()竞态 -
seen.add(x)返回true表示首次加入,正好作为filter条件 - 不建议用
Collectors.toMap()+Function.identity(),容易因重复 key 抛IllegalStateException
distinct() 在 List、Set、Map 转换中的性能差异
distinct() 本身内部用的是 LinkedHashSet 缓存已见元素,所以时间复杂度是 O(n),但会额外占用 O(n) 空间。实际效果受上游数据结构影响很大:
- 从
ArrayList流式去重:典型场景,无额外开销 - 从已有
HashSet开始流:此时再调distinct()完全冗余,纯属浪费 CPU 和内存 - 从数据库查出的
List:每个Map默认无合理equals(),distinct()几乎无效,应先转成确定类型的对象再处理
替代方案:用 TreeSet 自定义去重逻辑(适合排序+去重)
如果既要按某字段去重,又希望结果有序(比如按 id 升序),TreeSet 比 “filter + Set 记录” 更简洁:
Setunique = list.stream() .collect(Collectors.toCollection( () -> new TreeSet<>(Comparator.comparing(User::getName)) ));
- 注意:这会丢弃原始顺序;若需保持首次出现顺序,不能用此法
-
TreeSet构造时传入的Comparator决定“重复”的含义 —— 两个元素比较结果为 0 即视为重复 - 该方式在元素量大且已知需排序时,比先
distinct()再sorted()略高效(一次遍历完成两件事)
distinct()、filter+Set 还是 toMap,比堆砌技巧重要得多。










