Java Stream的distinct去重原理分析

2025-06-22 17:50

本文主要是介绍Java Stream的distinct去重原理分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《JavaStream的distinct去重原理分析》Javastream中的distinct方法用于去除流中的重复元素,它返回一个包含过滤后唯一元素的新流,该方法会根据元素的hashcode和eq...

一、distinct 的基础用法与核心特性

distinct()是 Stream API 中的有状态中间操作,用于移除流中的重复元素,其底层依赖元素的hashCode()equals()方法。用法示例:

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4);
List<Integer> unique = numbers.stream()
    .distinct()
    .collect(Collectors.toList());  // [1, 2, 3, 4]

核心特性

  • 去重逻辑基于元素的唯一性标识,而非内存地址;
  • 保持元素首次出现的顺序;
  • 属于有状态操作,处理过程中需维护已出现元素的集合。

二、distinct 的底层实现原理

1. 顺序流中的去重实现

顺序流中,distinct()通过HashSet存储已处理元素,流程如下:

  • 遍历流中的每个元素;
  • 对每个元素计算hashCode(),检查HashSet中是否存在相同哈希值的元素;
  • 若存在,进一步通过equals()比较内容,相同则过滤;
  • 若不存在,将元素添加到HashSet并保留在流中。

源码关键片段(JDK 17):

// ReferencePipeline.Java
public final Stream<P_OUT> distinct() {
    return new DistinctOps<P_OUT, P_OUT>(this);
}
 
// DistinctOps.java
@Override
public void accept(P_OUT t) {
    if (set.add(t)) {  // 调用HashSet的add方法,返回false表示重复
        down.accept(t);
    }
}

2. 并行流中的去重优化

并行流中,distinct()使用ConcurrentHashMap或分段处理提升性能:

  • 将流分割为多个子任务,每个子任务维护独立的HashSet
  • 子任务处理完成后,合并所有HashSet的结果;
  • 合并时使用HashMap去重,避免并发冲突。

并行处理示意图

+----------------+     +----------------+     +----------------+
|  子任务1: HashSet |---->|  子任务2: HashSet |---->|  合并阶段: HashMap |
|  存储元素A,B,C   |     |  存储元素B,D,E   |     |  最终结果A,B,C,D,E |
+----------------+     +----------------+     +----------------+

三、去重逻辑的核心依赖:hashCode 与 equals

1. 自定义对象的去重规则

若需对自定义对象去重,必须正确重写hashCode()equals()

class User {
    private String id;
    private String name;
    
    @Override
    public int hashCode() {
        return Objects.hash(id);  // 仅用id计算哈希值
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);  // 仅比较id
    }
    // 其他方法省略
}
 
// 使用示例
List<User> users = Arrays.asList(
    new User("1", "Alice"),
    new User("1", "Bob"),  // id相同,会被去重
    new User("2", "Charlie")
);
List<User> uniqueUsers = users.stream()
    .distinct()
    .collect(Collectors.toList());  // 保留两个用户

2. 常见误区:仅重写 equals 不重写 hashCode

若只重写equals,会导致去重失效,因为HashSet首先通过hashCode判断元素是否存在:

class ErrorUser {
    private String id;
    // 错误:未重写hashCode
    @Override
    public boolean equals(Object o) {
        // 正确实现equals...
    }
}
// 使用distinct时,两个id相同的ErrorUser可能因hashCode不同被视为不同元素

四、distinct android的性能影响与优化策略

1. 性能损耗的主要原因

  • 内存占用:需存储所有已出现元素,大数据集可能导致 OOM;
  • 哈希计算开销:每个元素需计算hashCode并进行哈希表查找;
  • 并行流的合并开销:多线程环境下的集合合并操作耗时。

2. 大数据集的去重优化

  • 预排序 + 相邻去重:对有序流使用distinct()效率更高,因重复元素相邻时哈希表查找次数减少
// 优化前:无序流去重
List<Integer> randomData = getRandomNumbers(1000000);
randomData.stream().distinct().count();  // 全量哈希表查找
 
// 优化后:先排序再去重
randomData.stream()
    .sorted()
    .distinct()
    .count();  // 相邻重复元素只需一次比较
  • 使用 Primitive Stream 减少装箱
// 低效:对象流装箱
Stream<Integer> boxedStream = data.stream().distinct();
 
// 高效:IntStream直接操作
IntStream primitiveStream = data.stream().mapToInt(Integer::intValue).distinct();
  • 分块处理大集合:避免一次性加载所有元素到内存
// 分块去重示例
int chunkSize = 100000;
List<Integer> result = new ArrayList<>();
for (int i = 0; i < data.size(); i += chunkSize) {
    int end = Math.min(i + chunkSize, data.size());
    List<Integer> chunk = data.subList(i, end);
    result.addAll(chunk.stream().distinct().collect(Collectors.toList()));
}
// 最后再去重一次合并结果
List<Integer> finalResult = result.stream().distinct().collect(Collectors.toList());

3. 并行流去重的参数调优

通过自定义Spliterator控制分块大小,减少合并开销:

class EfficientSpliterator implements Spliterator<Integer> {
    private final List<Integer> list;
    private int index;
    private static final int CHUNK_SIZE = 10000;  // 分块大小
    
    public EfficientSpliterator(List<Integer> list) {
        this.list = list;
        this.index = 0;
    }
    
    @Override
    public Spliterator<Integer> trySplit() {
        int size = list.size() - index;
        if (size < CHUNK_SIZE) return null;
        int splitPos = index + size / 2;
        Spliterator<Integer> spliterator = 
            new EfficientSpliterator(list.subList(index, splitPos));
        index = splitPos;
        return spliterator;
    }
    // 其他方法省略...
}
 
// 使用示例
List<Integer> data = ...;
Stream<Integer> optimizedStream = StreamSupport.stream(
    new EfficientSpliterator(data), true);  // 启用并行

五、特殊场景的去重方案

1. 基于部分属性的去重

若需根据对象的部分属性去重(而非全部属性),可结合mapcollect

class Product {
    private String id;
  China编程  private String name;
    private double price;
    // 构造器、getter省略
}
 
// 按id去重
List<Product> uniqueProducts = products.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toMap(Product::getId, p -> p, (p1, p2) -> p1),
        map -> new ArrayList<>(map.values())
    ));

2. 去重并保留最新元素

在日志等场景中,需按时间戳去重并保留最新记录:

class LogEntry {
    private String message;
    private long timestamp;
    // 构造器、getter省略
}
 
List<LogEntry> latestLogs = jslogs.stream()
    .collect(Collectors.toMap(
        LogEntry::getMessage, 
        entry -> entry, 
        (oldEntry, newEntry) -> newEntry.getTimestamp() > oldEntry.getTimestamp() 
            ? newEntry : oldEntry
    ))
    .values()
    .stream()
    .collect(Collectors.toList());

3. 模糊去重(非精确匹配)

如需基于相似度去重(如字符串编辑距离),需自定义去重逻辑:

List<String> fuzzyUnique = strings.stream()
    .filter(s -> !strings.stream()
        .anyMatch(t -> s != t && levenshteinDistance(s, t) < 2))
    .collect(Collectors.toList());

六、性能对比:distinct 与其他去重方式

去重方式大数据集性能内存占用实现复杂度适用场景
Stream.distinct()高(存储所有元素)通用去重
先排序 + 相邻去重有序数据去重
HashSet 直接去重简单集合去python
分块去重超大数据集去重

总结

distinct()作为 Stream API 中的基础操作,其核心去重逻辑依赖于hashCode()equals()的正确实现,而性能优化的关键在于:

  • 数据有序性利用:先排序再去重可减少哈希表查找次数;
  • 内存占用控制:对大数据集采用分块处理,避免一次性存储所有元素;
  • 基础类型优化:使用IntStream等避免装箱损耗;
  • 并行处理调优:通过自定义Spliterator控制分块大小,减少合并开销。

理解distinct()的底层实现原理,不仅能避免自定义对象去重时的常见错误,更能在处理大规模数据时选择合适的优化策略。记住:去重操作的本质是空间与时间的权衡,根据具体业务场景(数据规模、有序性、精确性要求)选择最优方案,才能实现性能与功能的平衡。

以上就是Java StreazRIiLLBm的distinct去重原理分析的详细内容,更多关于Java Stream distinct去重的资料请关注China编程(www.chinasem.cn)其它相关文章!

这篇关于Java Stream的distinct去重原理分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155172

相关文章

创建springBoot模块没有目录结构的解决方案

《创建springBoot模块没有目录结构的解决方案》2023版IntelliJIDEA创建模块时可能出现目录结构识别错误,导致文件显示异常,解决方法为选择模块后点击确认,重新校准项目结构设置,确保源... 目录创建spChina编程ringBoot模块没有目录结构解决方案总结创建springBoot模块没有目录

Java中的volatile关键字多方面解析

《Java中的volatile关键字多方面解析》volatile用于保证多线程变量可见性与禁止重排序,适用于状态标志、单例模式等场景,但不保证原子性,相较synchronized更轻量,但需谨慎使用以... 目录1. volatile的作用1.1 保证可见性1.2 禁止指令重排序2. volatile的使用

springboot项目中集成shiro+jwt完整实例代码

《springboot项目中集成shiro+jwt完整实例代码》本文详细介绍如何在项目中集成Shiro和JWT,实现用户登录校验、token携带及接口权限管理,涉及自定义Realm、ModularRe... 目录简介目的需要的jar集成过程1.配置shiro2.创建自定义Realm2.1 LoginReal

SpringBoot集成Shiro+JWT(Hutool)完整代码示例

《SpringBoot集成Shiro+JWT(Hutool)完整代码示例》ApacheShiro是一个强大且易用的Java安全框架,提供了认证、授权、加密和会话管理功能,在现代应用开发中,Shiro因... 目录一、背景介绍1.1 为什么使用Shiro?1.2 为什么需要双Token?二、技术栈组成三、环境

Java 与 LibreOffice 集成开发指南(环境搭建及代码示例)

《Java与LibreOffice集成开发指南(环境搭建及代码示例)》本文介绍Java与LibreOffice的集成方法,涵盖环境配置、API调用、文档转换、UNO桥接及REST接口等技术,提供... 目录1. 引言2. 环境搭建2.1 安装 LibreOffice2.2 配置 Java 开发环境2.3 配

在 Spring Boot 中连接 MySQL 数据库的详细步骤

《在SpringBoot中连接MySQL数据库的详细步骤》本文介绍了SpringBoot连接MySQL数据库的流程,添加依赖、配置连接信息、创建实体类与仓库接口,通过自动配置实现数据库操作,... 目录一、添加依赖二、配置数据库连接三、创建实体类四、创建仓库接口五、创建服务类六、创建控制器七、运行应用程序八

基于Spring Boot 的小区人脸识别与出入记录管理系统功能

《基于SpringBoot的小区人脸识别与出入记录管理系统功能》文章介绍基于SpringBoot框架与百度AI人脸识别API的小区出入管理系统,实现自动识别、记录及查询功能,涵盖技术选型、数据模型... 目录系统功能概述技术栈选择核心依赖配置数据模型设计出入记录实体类出入记录查询表单出入记录 VO 类(用于

深入解析Java NIO在高并发场景下的性能优化实践指南

《深入解析JavaNIO在高并发场景下的性能优化实践指南》随着互联网业务不断演进,对高并发、低延时网络服务的需求日益增长,本文将深入解析JavaNIO在高并发场景下的性能优化方法,希望对大家有所帮助... 目录简介一、技术背景与应用场景二、核心原理深入分析2.1 Selector多路复用2.2 Buffer

Java中数组与栈和堆之间的关系说明

《Java中数组与栈和堆之间的关系说明》文章讲解了Java数组的初始化方式、内存存储机制、引用传递特性及遍历、排序、拷贝技巧,强调引用数据类型方法调用时形参可能修改实参,但需注意引用指向单一对象的特性... 目录Java中数组与栈和堆的关系遍历数组接下来是一些编程小技巧总结Java中数组与栈和堆的关系关于

SpringBoot利用树形结构优化查询速度

《SpringBoot利用树形结构优化查询速度》这篇文章主要为大家详细介绍了SpringBoot利用树形结构优化查询速度,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一个真实的性能灾难传统方案为什么这么慢N+1查询灾难性能测试数据对比核心解决方案:一次查询 + O(n)算法解决