龙空技术网

你了解Java 流中的惰性求值吗?

学堂湾 116

前言:

当前小伙伴们对“python短路求值和惰性求值”大约比较关注,兄弟们都想要了解一些“python短路求值和惰性求值”的相关知识。那么小编在网络上搜集了一些对于“python短路求值和惰性求值””的相关知识,希望大家能喜欢,咱们快快来学习一下吧!

Java 8 中引入的Java Streams彻底改变了我们在 Java 中处理数据集合的方式。它们提供了一种高级的、声明性的数据处理方法,但它们最有趣的功能之一是惰性求值。本文深入探讨了惰性求值在 Java Streams 上下文中的含义以及它的好处,并附有实际示例。

Java 流基础知识

Java Streams 提供了一种顺序或并行处理元素序列的方法。流管道由源(如集合)、后跟零个或多个中间操作和终端操作组成。

中间操作:这些操作(例如filtermapsorted)将流转换为另一种流,并且是惰性操作。终端操作:产生结果或副作用的操作(如forEachcollect和)。reduce执行终端操作后,该流将无法再使用。什么是惰性评估?

惰性求值意味着仅在必要时才执行对流元素的计算,通常是在终端操作时。这与立即执行计算的急切求值相反。

Java 流中的惰性求值

在 Java Streams 中,直到调用终端操作后才会执行中间操作。尤其是对于大型数据集来说,这种方法可以通过减少迭代和计算次数来优化性能。

JVM的作用

JVM 在编排Java 流管道中的延迟计算方面发挥着至关重要的作用。它的内部工作原理如下:

流初始化:当您创建流(例如stream()在集合上)时,JVM 会为流设置初始配置,包括对源数据(例如集合或数组)的引用。中间操作:当您在流上链接中间操作(例如filter, 、map)时,JVM 通过创建与前一个流对象关联的新流对象来构建操作管道。但是,它在此阶段不执行任何计算。终端操作调用:当调用终端操作时,延迟计算就会发挥作用。此时,JVM触发整个管道开始处理。它通过遍历从源到终端操作的管道并在运行过程中应用中间操作来实现这一点。处理元素:在处理元素时,JVM 通过一次获取和处理一个元素来优化性能和内存使用。这可以确保不必要的元素不会加载到内存中,特别是在处理大型集合时。短路:对于支持短路的操作,例如findFirst或limit,一旦满足所需条件,JVM 就会停止处理。此行为减少了不必要的计算,使流处理更加高效。示例 1:基本惰性操作

在此示例中,仅当终端操作forEach开始时才执行filter操作。

List<String> strings = Arrays.asList("one", "two", "three", "four");Stream<String> longStringsStream = strings.stream().filter(s -> {    System.out.println("Filtering: " + s);    return s.length() > 3;});System.out.println("Stream created, filter not applied yet!");longStringsStream.forEach(System.out::println);
示例 2:组合多个惰性操作

在此示例中,每个元素都要经过filtermap,但仅在forEach操作执行时才执行。

strings.stream()       .filter(s -> {           System.out.println("Filter: " + s);           return s.length() > 3;       })       .map(s -> {           System.out.println("Map: " + s);           return s.toUpperCase();       })       .forEach(s -> System.out.println("Processed: " + s));
示例 3:无限流

此示例创建无限的自然数流,过滤偶数,并将输出限制为前 10 个偶数。

Stream.iterate(0, n -> n + 1)      .filter(n -> n % 2 == 0)      .limit(10)      .forEach(System.out::println);
例4:终端操作(执行触发)

延迟计算将持续到调用终端操作为止。终端操作是触发数据处理的操作。示例包括collectforEachreduce

当调用终端操作时,JVM 启动数据处理管道,并发生以下情况:

JVM 开始迭代源数据(例如,数字列表)。它按照指定的顺序一一应用记录的中间操作。计算并返回结果,或者执行终端操作中指定的最终操作。

List<Integer> result = filteredStream.collect(Collectors.toList());

这里,collect是一个终端操作,触发整个管道的执行。JVM 迭代源列表,应用filtermap转换,并将过滤和映射的值收集到新列表中。

示例 5:短路(效率)

Java流还支持短路操作。一旦满足特定条件,这些操作就会停止处理。例如,findFirstfindAny、 和limit是短路操作。

Optional<Integer> firstEven = numbers.stream()    .filter(n -> n % 2 == 0)    .findFirst();

在这种情况下,如果在流的早期发现偶数,则处理立即停止,这是一种效率优化。

示例 6:自定义 Spliterator

自定义分割器允许指定如何将流划分为更小的片段以进行并行处理。为了说明这个概念,让我们为名为“Range”的数据结构创建一个自定义 Spliterator 示例,表示整数范围。我们的目标是创建一个包含该范围内所有数字的流,并将其分成更小的部分以实现并行处理。实现这一点需要为“范围”数据结构实现一个自定义 Spliterator。

想象一下,有一个称为“范围”的独特数据结构,它定义了带有起点和终点的整数范围。目标是构建一个涵盖该范围内所有整数的流,并将其有效地划分为更小的段以促进并行计算。这可以通过设计一个针对“范围”数据结构定制的自定义 Spliterator 来实现。

import java.util.Spliterator;import java.util.function.Consumer;class Range {    private final int start;    private final int end;    public Range(int start, int end) {        this.start = start;        this.end = end;    }    public int getStart() {        return start;    }    public int getEnd() {        return end;    }}class RangeSpliterator implements Spliterator<Integer> {    private final Range range;    private int current;    public RangeSpliterator(Range range) {        this.range = range;        this.current = range.getStart();    }    @Override    public boolean tryAdvance(Consumer<? super Integer> action) {        if (current <= range.getEnd()) {            action.accept(current);            current++;            return true;        }        return false;    }    @Override    public Spliterator<Integer> trySplit() {        int mid = (current + range.getEnd()) / 2;        if (current >= mid) {            return null; // No more splitting        }        int start = current;        int end = mid;        current = mid + 1;        return new RangeSpliterator(new Range(start, end));    }    @Override    public long estimateSize() {        return range.getEnd() - current + 1;    }    @Override    public int characteristics() {        return SIZED | SUBSIZED | NONNULL | IMMUTABLE;    }}

在这个例子中:

“范围”表示整数范围的自定义数据结构。“RangeSpliterator”是一个定制的 Spliterator,负责将范围分割成更易于管理的部分。

我们可以利用这个自定义 Spliterator 创建一个流并同时处理它:

public class CustomSpliteratorExample {    public static void main(String[] args) {        Range range = new Range(1, 100);        Stream<Integer> parallelStream = StreamSupport.stream(            new RangeSpliterator(range), true); // We use 'true' to enable parallel processing        parallelStream            .parallel() // This line is optional but explicitly activates parallel processing            .forEach(System.out::println);    }}

总的来说,自定义 Spliterator 通过实现高效的并行处理以及数据拆分和处理方式的自定义来补充 Java 流中的惰性求值。延迟评估可确保将转换和操作推迟到必要时,并且自定义 Spliterator 指示应如何对数据进行分区以进行并行执行,从而实现高效且优化的流处理。自定义 Spliterator 和惰性求值之间的这种协同作用有助于提高 Java 流操作的整体效率和灵活性。

优点和注意事项性能优化:对于大型数据集,仅处理所需的数据即可带来显着的性能提升。内存效率:延迟计算允许处理完全实现后无法放入内存的数据流。灵活性:可以构建高效且可读的复杂流管道。注意事项:操作顺序很重要。此外,在与流一起使用的 lambda 表达式中应避免副作用。先进的惰性评估技术自定义惰性操作:高级用户可以通过实现自定义 Spliterator 或flatMap来创建惰性操作。无限流:惰性求值使得使用无限流成为可能。您可以生成或迭代无限流,并且该流将仅处理终端操作所需的元素。实际应用数据处理:惰性评估非常适合大数据处理等数据集太大而无法在内存中处理的场景。Web 服务:处理来自 Web 服务的分页结果时,您可以仅延迟处理所需的结果页面。结论

理解和利用Java Streams 中的惰性求值对于编写高效、有效且优雅的 Java 代码至关重要。它允许开发人员编写更具表现力、简洁和高性能的数据处理代码,使 Java 成为处理复杂数据处理任务的更强大的工具。

注:本文为翻译文,作者是DZone的 Andrei Tuchin。

标签: #python短路求值和惰性求值