首页 > 编程语言 >如何提高 Java Stream 遍历集合效率

如何提高 Java Stream 遍历集合效率

时间:2024-10-08 11:44:18浏览次数:1  
标签:遍历 Java Stream 迭代 处理 集合 操作

在 Java8 之前,对于大数据量的集合,传统的遍历方式主要是通过 for 循环或者 Iterator 迭代。然而,这种方式在处理大数据量集合时效率并不理想。以电商系统中的订单表为例,通常使用用户 ID 的 Hash 值来实现分表分库,以减少单个表的数据量,提高用户查询订单的速度。但当后台管理员审核订单时,需要将各个数据源的数据查询到应用层之后进行合并操作。比如,查询出过滤条件下的所有订单,并按照某个条件进行排序。在 Java8 之前,通常是通过 for 循环或者 Iterator 迭代来重新排序合并数据,或者通过重新定义 Collections.sort 的 Comparator 方法来实现。但这两种方式对于大数据量系统来说,效率低下。假设一个电商系统中有大量的订单数据需要处理,使用传统的遍历方式,随着数据量的增加,遍历所需的时间会呈线性增长。例如,当有 10 万个订单数据需要遍历筛选并排序时,可能需要花费数秒甚至更长的时间。而且传统方式的代码相对复杂,不够简洁,容易出错。此外,传统方式在处理多数据源的数据合并和排序时,需要手动管理遍历的过程,增加了开发的难度和维护成本。对于开发者来说,不仅要关注遍历的逻辑,还要处理各种边界情况和异常情况,使得代码的可读性和可维护性降低。

Stream 的优势初现

  1. 简洁强大的示例

假设我们有一个学生列表,包含学生的姓名和年龄等信息。使用传统的遍历方式来筛选出年龄大于 18 岁的学生并进行分组可能会涉及到复杂的循环和条件判断。但使用 Java Stream 结合 Lambda 表达式可以轻松实现这个功能。
import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.stream.Collectors;
class Student { private String name; private int age;
public Student(String name, int age) { this.name = name; this.age = age; }
public String getName() { return name; }
public int getAge() { return age; }}
public class StreamExample { public static void main(String[] args) { List<Student> students = new ArrayList<>(); students.add(new Student("小明", 17)); students.add(new Student("小红", 19)); students.add(new Student("小刚", 20));
Map<Boolean, List<Student>> groupedStudents = students.stream() .collect(Collectors.groupingBy(student -> student.getAge() > 18));
System.out.println("年龄大于 18 岁的学生:"); groupedStudents.get(true).forEach(student -> System.out.println(student.getName())); }}
通过这个例子可以看出,Stream 结合 Lambda 表达式使得代码更加简洁易懂,同时也提高了开发效率。
  1. 类似数据库操作

Stream 的聚合操作与数据库 SQL 的聚合操作(如 sorted、filter、map 等)类似。我们在应用层就可以高效地实现类似数据库 SQL 的聚合操作。例如,我们可以像在数据库中使用 SQL 查询语句一样,使用 Stream 对集合进行筛选、排序和映射等操作。Stream 不仅可以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据,提高数据的处理效率。比如在处理大量数据时,可以使用parallelStream()方法来创建并行流,从而充分利用多核处理器的优势。假设我们有一个包含 100 万个整数的列表,使用串行流和并行流分别对其进行筛选和求和操作。根据实际测试,在某些情况下,并行流可以大大缩短处理时间。例如,在一台具有四核处理器的计算机上,处理时间可能从串行流的几分钟缩短到并行流的几十秒。这充分体现了 Stream 在处理大数据量时的优势。

Stream 如何优化遍历

  1. 操作分类

Stream 的操作分为中间操作和终结操作。中间操作只对操作进行记录,返回一个流而不进行计算。中间操作又分为无状态操作和有状态操作。无状态操作指元素的处理不受之前元素的影响,比如filter操作;有状态操作指该操作只有拿到所有元素之后才能继续下去,如sorted操作。终结操作实现了计算操作,又分为短路操作和非短路操作。短路操作遇到某些符合条件的元素就可以得到最终结果,例如findFirst;非短路操作必须处理完所有元素才能得到最终结果,如forEach。这种分类构成了高效的处理管道,中间操作的 “懒操作” 特性结合终结操作和数据源,使得 Stream 能够高效地处理大数据集合。
  1. 源码实现

Stream 包主要由几个重要的结构类组成。BaseStream和Stream为最顶端的接口类。BaseStream主要定义了流的基本接口方法,如spliterator、isParallel等。Stream则定义了一些流的常用操作方法,例如map、filter等。ReferencePipeline是一个结构类,通过定义内部类组装各种操作流,它定义了Head、StatelessOp、StatefulOp三个内部类,实现了BaseStream与Stream的接口方法。Sink接口定义了每个 Stream 操作之间关系的协议,包含begin()、end()、cancellationRequested()、accpt()四个方法。ReferencePipeline最终将整个 Stream 流操作组装成一个调用链,而这条调用链上各个 Stream 操作的上下关系就是通过Sink接口协议来定义实现的。
  1. 操作叠加

一个 Stream 的各个操作是由处理管道组装,并统一完成数据处理的。在 JDK 中每次的中断操作会以使用阶段(Stage)命名。管道结构通常是由ReferencePipeline类实现的,它包含了Head、StatelessOp、StatefulOp三种内部类。Head类主要用来定义数据源操作,初次调用names.stream()方法时,会加载Head对象,此时为加载数据源操作;接着加载中间操作,分别为无状态中间操作StatelessOp对象和有状态操作StatefulOp对象,此时的 Stage 并没有执行,而是通过AbstractPipeline生成一个中间操作 Stage 链表;当调用终结操作时,会生成一个最终的 Stage,通过这个 Stage 触发之前的中间操作,从最后一个 Stage 开始,递归产生一个 Sink 链。
  1. 实例分析

例如,我们有一个包含姓名的集合,现在要找出最长且以 “张” 为姓氏的名字。使用传统的方式可能需要多次遍历集合,进行复杂的条件判断和比较。但使用 Stream 可以这样实现:
import java.util.ArrayList;import java.util.List;import java.util.Optional;
public class StreamExample { public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("张三丰"); names.add("李四"); names.add("张无忌"); names.add("王五");
Optional<String> longestZhangName = names.stream() .filter(name -> name.startsWith("张")) .reduce((name1, name2) -> name1.length() > name2.length()? name1 : name2);
if (longestZhangName.isPresent()) { System.out.println("最长且以张为姓氏的名字:" + longestZhangName.get()); } }}
在这个例子中,Stream 操作流程并非表面上的多次遍历集合。首先,通过filter操作筛选出以 “张” 为姓氏的名字,这一步只是记录了操作,并没有真正遍历集合。然后,通过reduce操作进行比较,找到最长的名字。在这个过程中,Stream 利用其内部的高效处理方式,只在需要的时候才进行实际的计算,大大提高了遍历集合的效率。

Stream 的并行处理

  1. 结合 ForkJoin 框架

Java 8 的并行 Stream 在底层使用 ForkJoinTask 实现并行处理,充分利用了 CPU 的多核能力。ForkJoin 框架的原理是将一个大任务,通过递归拆分成很多小任务,每一个小任务就是一个线程来执行。在 Stream 的并行处理中,ForkJoin 框架起到了关键作用。当使用并行 Stream 时,初始数据会被分成多个小块,每个块包含一部分元素。例如,假设有一个包含 10000 个元素的集合,在并行处理时,ForkJoin 框架会根据 CPU 的核心数将这个集合分成若干个小的数据块。如果是在一个四核 CPU 的环境下,可能会将这个集合分成四个数据块。然后,各个处理器核心同时对不同的数据块执行相同的操作。每个处理器核心独立地处理分配给它的数据块,就像多个工人同时处理不同的任务一样。比如在处理一个对集合中元素进行求和的操作时,每个核心会分别对自己负责的数据块进行求和。最后,各个处理器核心处理完成后,将结果合并为最终结果。ForkJoin 框架会将各个核心处理后的数据块结果进行合并,得到整个集合的最终处理结果。在这个过程中,Stream 结合 ForkJoin 框架实现了高效的并行处理,大大提高了对大数据集合的处理效率。
  1. 性能测试对比

为了验证 Stream 的并行处理在不同环境下的性能表现,我们进行了一系列的性能测试。首先,在常规迭代的情况下,对于大数据量的集合,随着数据量的增加,遍历所需的时间会呈线性增长。例如,当有 100 万个整数需要进行求和操作时,使用常规的 for 循环可能需要花费较长的时间。假设在一台单核处理器的计算机上,处理时间可能需要几分钟甚至更长。然后,我们对比了 Stream 的串行迭代和并行迭代。在单核 CPU 的环境下,Stream 的串行迭代和常规迭代的性能表现可能相差不大。但是,在多核 CPU 的环境下,Stream 的并行迭代优势就明显体现出来了。例如,我们有一个包含 1000 万个整数的列表,分别使用常规迭代、Stream 串行迭代和并行迭代对其进行求和操作。在一台四核 CPU 的计算机上,常规迭代可能需要几分钟的时间,而 Stream 串行迭代可能会比常规迭代稍微快一些,但也需要较长的时间。然而,使用 Stream 的并行迭代,由于充分利用了多核处理器的优势,处理时间可能会缩短到几十秒甚至更短。通过性能测试对比,我们可以得出结论:在大数据循环迭代且多核 CPU 环境下,Stream 的并行迭代优势明显。它能够大大提高对大数据集合的处理效率,为开发者提供了一种高效的处理大数据的方式。

Tips

  1. 并行流(Parallel Streams)

Java 8引入了并行流,允许你在多核处理器上并行执行操作,从而潜在地提高性能。要将一个顺序流转换为并行流,你可以调用parallel()方法,或者直接在集合上调用parallelStream()

示例:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 使用并行流计算总和long sum = numbers.parallelStream().mapToInt(Integer::intValue).sum();
注意,并行并不总是更快,其效率取决于任务的性质和数据的大小。对于小数据集,由于并行处理的开销,顺序处理可能更快。
  1. 避免副作用

在使用Stream时,应尽量避免使用会修改外部状态的操作(即副作用)。这样可以确保流操作的可预测性和并行安全性。

不推荐的示例:

List<Integer> numbers = Arrays.asList(1, 2, 3);List<Integer> squares = new ArrayList<>();numbers.stream().forEach(n -> {    squares.add(n * n); // 这里修改了外部集合squares});

推荐的示例:

List<Integer> numbers = Arrays.asList(1, 2, 3);List<Integer> squares = numbers.stream().map(n -> n * n).collect(Collectors.toList());
  1. 合理选择终端操作

不同的终端操作有不同的性能特征。例如,reduce()操作可能比collect()更高效,尤其是在不需要构造复杂结果结构时。
  1. 利用短路操作

短路操作是指一旦满足某个条件就停止处理剩余元素的操作,如anyMatch()allMatch(), 和 findFirst()。这在处理大数据集时特别有用,因为它们可以在找到第一个匹配项后立即终止操作。

示例:

boolean hasEvenNumber = numbers.stream().anyMatch(n -> n % 2 == 0);
  1. 避免不必要的收集

在某些情况下,你可能不需要将结果收集到列表或其他集合中。如果只需要处理单个结果(如最大值、最小值),直接使用对应的函数更高效。

示例:

OptionalInt max = numbers.stream().mapToInt(Integer::intValue).max();
  1. 利用Stream的特化版本

对于特定类型的数据(如整数、长整数、双精度浮点数),使用特化的流(如IntStreamLongStreamDoubleStream)可以减少自动装箱/拆箱的开销,提高效率。

 

 

标签:遍历,Java,Stream,迭代,处理,集合,操作
From: https://www.cnblogs.com/azwz/p/18451374

相关文章

  • java_day10_Object、Scanner、String
    1、Object类java中所有的类默认都有一个共同的父类:Object==比较:1、比较的是两个基本数据类型的话,比较两个数值是否相等2、比较的是两个引用数据类型的话,比较的是两个对象的地址值是否相等成员方法:inthashCode()返回对象的哈希码值。可以看作地址值的另外......
  • java计算机毕业设计宠物商城网站(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着城市化进程的加速和人们生活水平的提高,宠物已成为许多家庭的重要成员。宠物市场的繁荣带动了宠物相关产业的发展,其中宠物商城网站作为线上购物的......
  • Java基础语法
    一入Java情几许?大家好,我是深山夕照深秋雨。本文主要介绍Java的基础语法第二部分变量,类型转换和运算符。一、变量详解变量里的数据在计算机中的底层原理1、数据在计算机底层都是采用二进制:使用0、1,按照逢2进1的规则表示数据来存储。2、算出一个数据的二进制形式:除二取余法......
  • Java大厂面试题合集!
    1、为什么要使用线程池  难度系数:⭐使用线程池的主要原因包括:降低资源开销:线程池预先创建一定数量的线程,当需要处理任务时,直接从线程池中获取已经创建好的线程,避免了频繁地创建和销毁线程所带来的开销。这样可以显著提高系统的性能。控制并发线程数量:线程池可以限制同......
  • Java中的外观模式
    Java中的外观模式综述本文总结外观模式的定义,特点,使用场景并给出了具体的示例.外观模式的定义外观模式(门面模式)是一种结构型设计模式.其主要目的是为复杂系统提供一个简化的接口.帮助客户端代码与系统的子系统进行交互,同时还可以省略大量的细节.这种设计模式可以称得......
  • 视野修炼-技术周刊第104期 | 下一代 JavaScript 工具链
    欢迎来到第104期的【视野修炼-技术周刊】,下面是本期的精选内容简介......
  • Java中的任务超时处理
    Java中的任务超时处理综述任务超时处理是编程世界中一个不可或缺且常见的处理机制,特别是在进行耗时任务时,如网络请求,数据库查询,大规模数据处理等场景。在这些情况下,为了防止任务因各种不可预测的因素(如网络延迟,服务器响应超时,数据量过大等)而无休止地占用宝贵的系......
  • Java中的策略模式
    Java中的策略模式综述本文总结了策略模式的定义,特点,使用场景以及实现思路。策略模式的定义策略模式说通了,就是定义一系列的算法,将它们各自封装起来,并且使用一个共同的接口使它们可相互替换.使得算法和算法之间没有耦合,这样如果方法需要修改或者添加,工程师不需要修......
  • Java基础第八章(多态)
    多态1.方法体现多态方法重载体现多态对象通过传入不同数量的参数,会调用不同的sun方法,体现出多态方法重写体现多态A类和B类是继承关系,通过不同对象调用对应类中重写的方法体现2.对象体现多态编译是javac,运行是java(1)一个对象的编译类型和运行类型可以不一致将父......
  • JavaScript 小知识:轻松搞定 ArrayBuffer 到 Base64 的转换
    关键词:ArrayBuffer,Base64,栈溢出,TextDecoder,btoa,性能优化,JavaScript,兼容性摘要本文探讨了在JavaScript中将ArrayBuffer转换为Base64字符串时遇到的栈溢出问题,并提供了几种实用的解决方案。我们将通过生动的比喻来解释相关概念,比较不同方法的性能和兼......