0

0

Java 8中的Stream API有哪些常用操作?它是惰性求值的吗?

betcha

betcha

发布时间:2025-09-04 16:11:01

|

710人浏览过

|

来源于php中文网

原创

答案:Java 8的Stream API通过中间操作和终端操作实现惰性求值,提升性能与代码可读性。中间操作如filter、map返回新流且惰性执行,终端操作如forEach、collect触发计算并产生结果。惰性求值避免不必要的计算,支持短路操作,优化管道处理,适用于无限流。使用时需避免副作用、重复使用流、不当处理Optional及滥用并行流,推荐保持操作纯粹、正确关闭资源。

java 8中的stream api有哪些常用操作?它是惰性求值的吗?

Java 8的Stream API确实为我们处理集合数据提供了一套强大而优雅的工具集,它主要包含两大类操作:中间操作(如

filter
map
sorted
)和终端操作(如
forEach
collect
reduce
)。这些操作使得数据处理流程化、声明式,极大地提升了代码的可读性和简洁性。更关键的是,Stream API是惰性求值的,这意味着中间操作并不会立即执行,而是等待一个终端操作被调用时才真正启动计算,这种机制带来了显著的性能优化和设计灵活性。

解决方案

在我看来,理解Stream API的核心在于掌握其操作分类和惰性求值的哲学。它不仅仅是写出更短的代码,更是改变了我们思考数据处理的方式。

Stream API的常用操作

Stream API的操作可以大致分为两类:

立即学习Java免费学习笔记(深入)”;

  1. 中间操作(Intermediate Operations)

    • 这些操作会返回一个新的
      Stream
      对象,因此可以进行链式调用。
    • 它们是“惰性”的,这意味着它们不会立即执行任何计算,只是构建一个操作管道。
    • 常见的中间操作包括:
      • filter(Predicate predicate)
        :根据给定条件过滤元素。比如,筛选出所有偶数。
        List numbers = Arrays.asList(1, 2, 3, 4, 5);
        numbers.stream().filter(n -> n % 2 == 0); // 得到 Stream<2, 4>
      • map(Function mapper)
        :将流中的每个元素转换成另一种类型。例如,将数字平方。
        numbers.stream().map(n -> n * n); // 得到 Stream<1, 4, 9, 16, 25>
      • flatMap(Function> mapper)
        :将流中的每个元素转换成一个流,然后将这些流扁平化成一个流。这在处理嵌套集合时非常有用。
        List> sentences = Arrays.asList(
            Arrays.asList("Hello", "World"),
            Arrays.asList("Java", "Stream")
        );
        sentences.stream().flatMap(Collection::stream); // 得到 Stream<"Hello", "World", "Java", "Stream">
      • distinct()
        :去除流中的重复元素。
      • sorted()
        sorted(Comparator comparator)
        :对流中的元素进行排序。
      • peek(Consumer action)
        :对流中的每个元素执行一个操作,但不会改变流本身。这在调试时特别有用,可以观察流在某个阶段的状态。
      • limit(long maxSize)
        :截断流,使其元素不超过给定数量。
      • skip(long n)
        :跳过流中的前n个元素。
  2. 终端操作(Terminal Operations)

    • 这些操作会消耗流,并产生一个非
      Stream
      的结果(如一个集合、一个值或一个副作用)。
    • 它们是“急切”的,一旦调用,就会触发整个Stream管道的执行。一个流只能有一个终端操作。
    • 常见的终端操作包括:
      • forEach(Consumer action)
        :对流中的每个元素执行一个操作,通常用于打印或触发副作用。
        numbers.stream().filter(n -> n % 2 == 0).forEach(System.out::println); // 打印 2, 4
      • collect(Collector collector)
        :将流中的元素收集到各种集合中,或者进行分组、分区等复杂操作。这是Stream API最强大的操作之一。
        List evens = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList()); // 得到 [2, 4]
        Map> partitioned = numbers.stream().collect(Collectors.partitioningBy(n -> n % 2 == 0)); // 得到 {true=[2, 4], false=[1, 3, 5]}
      • reduce(BinaryOperator accumulator)
        reduce(T identity, BinaryOperator accumulator)
        :将流中的元素通过一个累积函数归约为一个单一结果。比如,求和。
        Optional sum = numbers.stream().reduce(Integer::sum); // 得到 Optional[15]
        Integer sumWithIdentity = numbers.stream().reduce(0, Integer::sum); // 得到 15
      • count()
        :返回流中元素的数量。
      • min(Comparator comparator)
        /
        max(Comparator comparator)
        :根据给定比较器找到最小值或最大值。
      • anyMatch(Predicate predicate)
        /
        allMatch(Predicate predicate)
        /
        noneMatch(Predicate predicate)
        :检查流中是否有任何元素、所有元素或没有元素匹配给定条件。这些是短路操作。
      • findFirst()
        /
        findAny()
        :返回流中的第一个或任意一个元素(通常用于并行流)。

Stream API的惰性求值

是的,Stream API是惰性求值的(Lazy Evaluation)。这意味着当你调用一个中间操作时,它并不会立即处理数据,而是仅仅记录下这个操作,并返回一个新的Stream。只有当一个终端操作被调用时,整个操作链才会被“激活”,数据才会从源头流过整个管道并进行计算。

这种机制带来了很多好处:

  • 效率提升:Stream API可以进行优化,只处理需要的数据。比如,如果你有一个很长的流,然后你调用
    filter
    limit(10)
    ,Stream可能只处理前面10个满足
    filter
    条件的元素就停止了,而不会遍历整个流。
  • 短路操作
    limit()
    ,
    findFirst()
    ,
    anyMatch()
    等操作可以在找到结果后立即停止处理,无需遍历整个流。
  • 无限流:惰性求值使得处理无限流成为可能,因为只有需要的部分才会被计算。

Stream API的中间操作与终端操作有何区别

在我看来,中间操作和终端操作之间的区别,是理解Stream API运作机制的关键。说白了,中间操作就像是你在规划一次旅行的路线图,你只是标记出要经过哪些地方,要怎么走,但你还没有真正出发。而终端操作,就是你真正踏上旅程,开始按照路线图行动,最终到达目的地。

中间操作的特性:

  • 返回Stream:它们总是返回一个新的
    Stream
    对象。这意味着你可以将多个中间操作串联起来,形成一个操作链(pipeline)。这种链式调用的设计,使得代码看起来非常流畅和声明式。
  • 惰性执行:这是最重要的一个特性。中间操作本身不会触发任何实际的数据处理。它们只是在构建一个“蓝图”或者说一个“查询计划”。数据并不会在调用
    filter
    map
    时立即被处理。
  • 零个或多个:在一个Stream管道中,你可以有零个、一个或多个中间操作。
  • 无副作用(通常):理想情况下,中间操作应该是无副作用的,即它们不应该修改流的源数据或外部状态。虽然
    peek
    可以用于副作用,但通常建议仅用于调试。
  • 分为有状态和无状态
    • 无状态操作:如
      filter
      map
      ,处理每个元素时不需要知道其他元素的状态。
    • 有状态操作:如
      sorted
      distinct
      ,它们需要处理整个流才能完成操作,因为它们的结果依赖于所有元素。例如,
      sorted
      必须知道所有元素才能进行排序,
      distinct
      需要知道之前出现过的所有元素来判断是否重复。

终端操作的特性:

  • 返回非Stream结果:终端操作不返回
    Stream
    ,而是返回一个具体的结果(如
    List
    、`
    Optional
    long
    void
    ),或者产生一个副作用。
  • 触发执行:一旦调用终端操作,整个Stream管道(包括所有的中间操作)就会被立即执行。这是Stream管道开始工作并产生结果的信号。
  • 有且仅有一个:一个Stream只能执行一次终端操作。一旦一个Stream被消耗(即执行了终端操作),它就不能再被重用。如果你尝试再次对同一个Stream执行终端操作,会抛出
    IllegalStateException
  • 副作用或结果:终端操作通常用于产生最终结果(如
    collect
    、`
    reduce
    count
    )或执行副作用(如
    forEach
    )。

举个例子,想象你有一箱苹果(源数据),你想找出其中红色的、没有虫子的,然后把它们切成片,最后装到一个篮子里。

  • filter(是红色的)
    filter(没有虫子)
    都是中间操作,你只是心里想了一下这个筛选过程,苹果还在箱子里。
  • map(切成片)
    也是中间操作,你只是想象了一下切片后的样子。
  • collect(装到篮子里)
    就是终端操作,你真正动手去筛选、去切片,最后把它们放进篮子,这时候箱子里的苹果才真正被处理。

惰性求值如何提升Stream API的性能和效率?

惰性求值是Stream API的性能基石,它让Stream在很多场景下比传统的迭代器循环更加高效,甚至能处理理论上的无限数据流。在我看来,这不仅仅是代码风格的改变,更是计算哲学上的一种优化。

1. 避免不必要的计算

这是惰性求值最直接的好处。Stream管道只有在终端操作被调用时才开始执行,并且会尽可能地延迟计算。这意味着,如果一个操作的结果在后续的管道中没有被用到,或者可以提前确定最终结果,那么这个操作甚至可能不会被完全执行。

考虑这样一个场景:

List names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
Optional foundName = names.stream()
    .filter(name -> {
        System.out.println("Filtering: " + name);
        return name.startsWith("C");
    })
    .map(name -> {
        System.out.println("Mapping: " + name);
        return name.toUpperCase();
    })
    .findFirst(); // 终端操作

如果你运行这段代码,你会发现输出可能是这样的:

标小兔AI写标书
标小兔AI写标书

一款专业的标书AI代写平台,提供专业AI标书代写服务,安全、稳定、速度快,可满足各类招投标需求,标小兔,写标书,快如兔。

下载
Filtering: Alice
Filtering: Bob
Filtering: Charlie
Mapping: Charlie

注意到了吗?

filter
操作只执行到"Charlie"就停止了,
map
操作也只对"Charlie"执行了一次。
findFirst()
是一个短路终端操作,一旦找到第一个匹配的元素,它就会立即停止整个Stream管道的执行,而不会继续处理"David"和"Eve"。如果不是惰性求值,
filter
map
可能会对所有元素都执行一遍,即使我们只需要第一个结果。

2. 短路操作的效率

limit()
findFirst()
anyMatch()
allMatch()
noneMatch()
等都是短路操作。它们利用惰性求值的特性,在满足条件时可以提前终止Stream的处理。这对于处理大型数据集或无限流时尤其关键。

  • limit(n)
    :如果我只需要前10个元素,Stream API不会处理超过10个元素。
  • anyMatch(predicate)
    :只要找到一个匹配的元素,就立即返回
    true
    ,无需检查其余元素。

3. 管道优化

由于中间操作不立即执行,Stream API有机会对整个操作管道进行优化。JVM可以在内部重排操作顺序,或者将多个操作合并成一个,以减少遍历次数和提高CPU缓存效率。例如,一个

filter
后面跟着一个
map
,JVM可能会将这两个操作合并成一个内部循环,而不是先完全过滤一遍再完全映射一遍。这种“融合”机制减少了中间数据结构的创建和内存开销。

4. 处理无限流

惰性求值是处理无限流(如

Stream.iterate()
Stream.generate()
创建的流)的唯一方式。因为只有在终端操作需要时,流才会生成并处理有限数量的元素,否则一个急切求值的无限流会立即导致内存溢出。

// 生成一个无限的偶数流,并取出前5个
Stream.iterate(0, n -> n + 2) // 无限流
      .limit(5)              // 短路中间操作
      .forEach(System.out::println); // 终端操作
// 输出:0, 2, 4, 6, 8

如果没有

limit
这样的短路操作和惰性求值,
iterate
会无限生成数字,耗尽内存。

总而言之,惰性求值让Stream API能够以一种更智能、更高效的方式处理数据。它将计算的责任从数据生成者转移到数据消费者,从而允许在需要时才进行计算,并提供灵活的优化机会。

在实际项目中,Stream API有哪些常见的陷阱或最佳实践?

Stream API虽然强大,但使用不当也可能引入一些意想不到的问题。我个人在实践中遇到过一些,也总结了一些经验,希望对大家有所帮助。

常见的陷阱:

  1. 修改源集合或外部状态(Side Effects)

    • 陷阱:在
      filter
      map
      等中间操作中尝试修改Stream的源集合或外部可变状态,这会导致并发修改异常或不可预测的行为,尤其是在并行流中。Stream API鼓励函数式编程范式,即操作应该是无副作用的。
    • 示例
      List names = new ArrayList<>(Arrays.asList("A", "B", "C"));
      names.stream().filter(s -> {
          // 错误示范:在filter中修改源集合
          // names.remove(s); // 会抛出 ConcurrentModificationException
          return true;
      }).forEach(System.out::println);
    • 最佳实践:保持中间操作的纯粹性,避免副作用。如果需要收集结果,使用
      collect
      。如果确实需要副作用,考虑
      forEach
      peek
      ,但要清楚其影响,并避免在并行流中使用共享的可变状态。
  2. 重复使用已消耗的Stream

    • 陷阱:Stream只能被消耗一次。一旦执行了终端操作,该Stream就关闭了,不能再进行任何操作。尝试重用会抛出
      IllegalStateException
    • 示例
      Stream myStream = Arrays.asList("a", "b", "c").stream();
      myStream.forEach(System.out::println); // 第一次消耗
      // myStream.count(); // 错误!会抛出 IllegalStateException
    • 最佳实践:如果需要多次操作,请每次都从源数据重新创建一个Stream。或者,将Stream的结果收集到一个集合中,然后对集合进行多次操作。
  3. Optional
    处理不当

    • 陷阱
      findFirst()
      min()
      max()
      等操作返回
      Optional
      类型,如果直接调用
      get()
      而没有检查
      isPresent()
      ,在流为空时会抛出
      NoSuchElementException
    • 示例
      List emptyList = Collections.emptyList();
      // 错误示范
      // Integer max = emptyList.stream().max(Integer::compare).get();
    • 最佳实践:始终使用
      Optional
      提供的安全方法,如
      orElse()
      orElseGet()
      orElseThrow()
      ifPresent()
      ,或者先调用
      isPresent()
      进行检查。
  4. 过度使用并行流(Parallel Stream)

    • 陷阱:并行流并非总是更快。对于小数据集、I/O密集型操作或有状态的中间操作(如
      sorted
      distinct
      ),并行流的开销(线程管理、数据分区、结果合并)可能远大于其带来的收益,甚至可能导致性能下降。此外,并行流更容易引入线程安全问题,特别是当操作包含副作用时。
    • 最佳实践:只在处理大数据集、CPU密集型操作,且操作本身是无状态或线程安全时考虑并行流。始终进行性能测试来验证并行流是否真的带来了提升。
  5. 不关闭资源

    • 陷阱:当Stream源自需要关闭的资源(如
      Files.lines()
      BufferedReader.lines()
      )时,如果没有显式关闭,可能会导致资源泄露。
    • 最佳实践:使用
      try-with-resources
      语句来确保Stream及其底层资源被正确关闭。
      try (Stream lines = Files.lines(Paths.get("myfile.txt"))) {
          lines.forEach(System.out::println);
      } catch (IOException e) {
          e.printStackTrace();
      }

最佳实践:

  1. 保持操作的纯粹性:让
    filter
    map
    等操作保持无副作用,专注于转换和过滤数据。这不仅符合函数式编程理念,也使得代码更易于理解、测试和并行化。
  2. 选择合适的终端操作:根据你的目标,选择最合适的终端操作。例如,如果只是遍历,用
    forEach
    ;如果需要聚合结果,用
    collect
    reduce
    ;如果只是检查条件,用
    anyMatch
    等。
  3. 善用
    peek()
    进行调试
    :当Stream管道变得复杂时,
    peek()
    是一个非常有用的调试工具,可以让你在不改变流的情况下,观察每个元素在管道中不同阶段的状态。
    numbers.stream()
        .filter(n -> n % 2 == 0)
        .peek(e -> System.out.println("Filtered element: " + e))
        .map(n -> n * n)

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

834

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

739

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

735

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

399

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

25

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.6万人学习

C# 教程
C# 教程

共94课时 | 6.9万人学习

Java 教程
Java 教程

共578课时 | 46.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号