当前位置:

首页 > 编程开发 > 集合去重效率对比:实战测试 Stream.distinct() 与 HashSet 处理变量的速度

集合去重效率对比:实战测试 Stream.distinct() 与 HashSet 处理变量的速度

对于普通变量去重,HashSet构造法速度最快且稳定,Stream.distinct()虽保留顺序但速度较慢,数据量越小差距越明显。实测显示10万条以内HashSet快5-10倍,百万级差距缩小。选择取决于是否需保序:无需保序用HashSet,需保序用LinkedHashSet优于Stream。

先抛结论:对于普通变量(比如 String、Integer 这类)的去重操作,HashSet 构造法是最快的,而且表现极其稳定;Stream.distinct() 能保住顺序,但速度会慢一截——数据量越小,差距越明显。这不是拍脑袋的理论推演,而是几轮实测下来的真实结果。

集合去重效率对比:实战测试 Stream.distinct() 与 HashSet 处理变量的速度

核心性能表现(基于真实测试数据)

下面几组数据是在不同数据规模和不同重复率下跑出来的典型耗时(单位:毫秒),测试环境是 JDK 17 + 普通字符串 List:

  • 1 万条数据,重复率 10%:HashSet ≈ 1 ms|LinkedHashSet ≈ 1–2 ms|Stream.distinct() ≈ 60 ms
  • 10 万条数据,重复率 1%:HashSet ≈ 6 ms|LinkedHashSet ≈ 11 ms|Stream.distinct() ≈ 53 ms
  • 100 万条数据,重复率 10%:HashSet ≈ 242 ms|LinkedHashSet ≈ 288 ms|Stream.distinct() ≈ 230 ms

换句话说:数据量越大,三个方法的差距越接近;但只要数据量还在 10 万以内,HashSet 基本上能稳压 Stream 5 到 10 倍的速度。而遇到重复率特别高(比如大量重复)且数据量极大的情况,差距会进一步收窄——这时候瓶颈早就不是算法本身了,而是内存带宽和 GC 压力。

为什么 HashSet 更快?

因为它玩的是“直给”操作:

  • new HashSet(list) 本质上就是一次性遍历完整个列表,然后把元素逐个插入哈希表,底层走的是 HashMap.put(),平均时间复杂度 O(1)
  • 没有任何多余的包装:不创建 Stream 对象、不触发 Spliterator、不走 Collector 管道、也没有中间状态缓存的开销
  • JVM 对 HashSet(Collection) 这个构造器做了非常深入的优化,甚至内部复用了 LinkedHashSet 的实现(源码里写得明明白白),但对外接口仍然是轻量级的

Stream.distinct() 的真实成本在哪?

它并不是“慢在去重”这个动作本身,而是慢在整个流式管道的启动和编排:

  • 每次调用 stream().distinct().collect(...),都要先构建一个 Stream 实例、绑定 Spliterator、初始化内部的 Set 缓存(实际是个 LinkedHashSet),然后再逐个调用 accept() 去处理元素
  • 哪怕你只想去重,它默认也会开启“稳定性保证”(即保留首次出现的顺序),这引入了额外的哈希+链表维护成本
  • 如果脑子一热在小数据上用了 parallelStream(),反而会因为线程调度的开销变得更慢;真正适合并行的场景是百万级以上且计算密集型的任务

选哪个?看你的需求优先级

不需要保持原始顺序 → 闭着眼睛用 new ArrayList(new HashSet(list)),最快最省事。

必须保留首次出现的顺序 → 用 new ArrayList(new LinkedHashSet(list)),比 Stream.distinct() 更快也更可控。

已经嵌在流式处理流程里(比如 filter/map 后面接去重)→ 用 distinct(),语义清晰,不会打断链式调用。

需要兼容 null 或自定义对象 → 两种方式都依赖正确实现 equals() 和 hashCode(),这点上没差别。

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发
相关文章 更多
c#如何使用FileStream_c#FileStream看这一篇就够了_保姆级教程
c#如何使用FileStream_c#FileStream看这一篇就够了_保姆级教程

FileStream适用于大文件、多进程共享及精确控制读写位置等场景,其构造函数需合理选择FileMode、FileAccess和FileShare参数。使用using确保及时释放句柄,避免文件占用。读写字符串时必须统一编码,否则会产生乱码。

c++如何读取内存中的压缩数据流_z_stream用法【深度】
c++如何读取内存中的压缩数据流_z_stream用法【深度】

使用z_stream解压内存数据需手动管理缓冲区与状态:先清零结构体,正确设置窗口位宽参数,然后循环调用inflate()函数直至返回Z_STREAM_END,期间动态扩容输出缓冲区,最后调用inflateEnd()清理,否则容易导致崩溃或数据不完整。

如何使用 ObjectOutputStream 序列化具有继承关系的复杂业务实体对象
如何使用 ObjectOutputStream 序列化具有继承关系的复杂业务实体对象

父类必须实现Serializable,否则子类序列化时抛出NotSerializableException异常;所有继承链上的类都应显式声明serialVersionUID,并遵循兼容性规则进行更新;transient和static字段在继承层次中同样影响序列化行为;构造函数在序列化过程中不会被调用,因此父类无参构造函数非必需。

C++实现字符串中单词的顺序翻转 _ 结合stringstream与reverse技巧【详解】
C++实现字符串中单词的顺序翻转 _ 结合stringstream与reverse技巧【详解】

使用std::stringstream按空格拆分单词存入vector,再通过std::reverse整体反转单词顺序,最后手动拼接并添加空格,注意避免首尾多余空格。该方法仅适用于ASCII字符串,不支持UTF-8等多字节字符,否则会因字节误分割导致乱码。

怎么利用 Stream.builder() 手动构建一个包含特定业务对象的流
怎么利用 Stream.builder() 手动构建一个包含特定业务对象的流

Stream.builder()适用于小批量确定值元素场景,需要先获取builder实例,通过add()逐个添加元素后调用build()生成流,泛型类型需显式指定。不适用于大批量填充,不建议转为并行流,但构建出的流支持limit、findFirst等常用短路操作。

怎么利用 Stream.anyMatch() 检查流中是否有满足条件的项
怎么利用 Stream.anyMatch() 检查流中是否有满足条件的项

anyMatch()是JavaStream的短路终端操作,当流中任意元素满足谓词时立即返回true,若流为空则返回false。请注意:不能与findFirst()或filter()链式调用,因为流被终端操作关闭后无法再次使用,会抛出异常。谓词内需防范null元素,应使用Objects.nonNull()判空以避免空指针异常。

如何利用 Stream.of() 快速构建一个简单的对象流
如何利用 Stream.of() 快速构建一个简单的对象流

Stream.of()适用于快速将多个明确对象转为流,避免集合中转。传数组会被视为单个元素,应改用Arrays.stream()或直接列元素。对null值应使用Stream.ofNullable()或手动判空。自定义对象流需注意覆盖基础方法,且流不可重复消费。

如何利用 Stream 的 parallelStream 配合自定义 ForkJoinPool 规避并行流对主线程池的阻塞
如何利用 Stream 的 parallelStream 配合自定义 ForkJoinPool 规避并行流对主线程池的阻塞

parallelStream默认使用全局共享的ForkJoinPool.commonPool,子任务阻塞会拖慢所有并行流。正确做法是创建自定义ForkJoinPool,将整个并行流作为任务提交执行。对I/O密集型任务,建议将阻塞操作包装进CompletableFuture,使用专用线程池分离职责。线程池并行度需根据任务类型合理设置,避免简单设为CPU核数。

Files.lines() 惰性读取:分析在处理 GB 级超大文本文件变量时利用 Stream 流规避内存溢出的策略
Files.lines() 惰性读取:分析在处理 GB 级超大文本文件变量时利用 Stream 流规避内存溢出的策略

Files.lines()看似惰性,但sorted()或collect()会全量加载导致OOM。安全用法:逐行forEach避免全量操作,用try-with-resources显式关闭并指定UTF-8编码,可结合缓冲批处理提升吞吐。

怎么通过 Stream.findAny() 在多核处理器上利用并行流优势快速碰撞出第一个匹配项
怎么通过 Stream.findAny() 在多核处理器上利用并行流优势快速碰撞出第一个匹配项

并行流findAny()利用多核并行与短路机制,任一线程找到结果立即终止,无需维护顺序,基于ForkJoinPool实现高效负载均衡。特别适用于数据量大且不关心顺序的场景,需通过parallelStream()启动才能触发并行处理,可显著提升性能。

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

PDF教程
PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

Mac软件 更多
Postman macOS版
Postman macOS版
Mac

Postman是一套面向API全生命周期的开发与测试平台,可在Mac上发送和调试多种协议请求,管理集合、环境及认证信息,并完成脚本测试、模拟服务、监控、文档发布和团队协作。它适合开发、测试及运维人员使用,提供个人免费方案。

Docker Desktop macOS版
Docker Desktop macOS版
Mac

Docker Desktop 是 Docker 官方为 Mac 提供的容器开发环境,集成 Docker Engine、命令行工具、Compose、Build、Kubernetes 与图形化管理界面。

Android Studio macOS版
Android Studio macOS版
Mac

Android Studio 是 Google 面向安卓应用开发提供的官方集成开发环境,基于 IntelliJ IDEA 构建。它整合智能代码编辑、Gradle 构建、界面预览、设备模拟、性能分析、调试测试及 APK 分析等工具。

WINDOWS 更多
PixPix 网页版
PixPix 网页版
Windows

PixPix 是面向品牌、卖家和内容团队的在线 AI 图片视频创作平台,提供图视频 Agent、无限画布、图片生成、视频生成、自然语言编辑及商品套图、产品精修和带货视频等电商工具。

Postman Windows版
Postman Windows版
Windows

Postman 是面向 API 全生命周期的开发与协作平台。Windows 桌面版可用于构建和发送 HTTP、GraphQL、gRPC、WebSocket、MQTT 等请求,并提供环境变量、集合、脚本测试、模拟服务、监控、文档及团队工作区等

Docker Desktop Windows版
Docker Desktop Windows版
Windows

Docker Desktop 为 Windows 开发者提供集成式容器环境,内置 Docker Engine、命令行工具、Compose、Build 与 Kubernetes 等组件,可在本机完成镜像构建、容器运行、日志查看和资源管理。