Java 8 流懒惰在实践中是无用的吗?

2022-09-03 08:57:03

我最近阅读了很多关于Java 8流的文章,以及几篇关于使用Java 8流延迟加载的文章:这里这里。我似乎无法摆脱延迟加载完全无用的感觉(或者充其量,一个提供零性能值的次要语法便利)。

让我们以这段代码为例:

int[] myInts = new int[]{1,2,3,5,8,13,21};

IntStream myIntStream = IntStream.of(myInts);

int[] myChangedArray = myIntStream
                        .peek(n -> System.out.println("About to square: " + n))
                        .map(n -> (int)Math.pow(n, 2))
                        .peek(n -> System.out.println("Done squaring, result: " + n))
                        .toArray();

这将登录到控制台中,因为 在这种情况下,将调用 ,并且我们的流是惰性的,并且仅在调用终端操作时执行。当然,我也可以这样做:terminal operationtoArray()

  IntStream myChangedInts = myIntStream
    .peek(n -> System.out.println("About to square: " + n))
    .map(n -> (int)Math.pow(n, 2))
    .peek(n -> System.out.println("Done squaring, result: " + n));

什么都不会打印出来,因为地图没有发生,因为我不需要数据。直到我称之为:

  int[] myChangedArray = myChangedInts.toArray();

瞧,我得到了我的映射数据,以及我的控制台日志。除了我看不到任何好处。我意识到我可以在调用之前很久就定义过滤器代码,并且我可以传递这个“不是真正过滤的流”),但那又如何呢?这是唯一的好处吗?toArray()

这些文章似乎暗示懒惰会带来性能提升,例如:

在Java 8 Streams API中,中间操作是懒惰的,并且其内部处理模型经过优化,使其能够以高性能处理大量数据。

Java 8 Streams API 在短路操作的帮助下优化了流处理。短路方法在满足其条件后立即结束流处理。用正常的话说,短路操作,一旦条件得到满足,就打破了所有的中间操作,躺在管道里。某些中间操作以及终端操作具有此行为。

这听起来就像打破了一个循环,与懒惰无关。

最后,第二篇文章中有这样一句令人困惑的话:

惰性操作可提高效率。这是一种不处理陈旧数据的方法。在逐步消耗输入数据而不是事先拥有整个完整元素集的情况下,惰性操作可能很有用。例如,考虑使用 Stream#generate(Supplier<T>) 创建无限流的情况,并且提供的 Supplier 函数正在逐渐从远程服务器接收数据。在这些情况下,服务器调用将仅在需要时在终端操作时进行。

不处理陈旧数据?什么?延迟加载如何防止某人处理过时的数据?


TLDR:除了能够在以后运行过滤器/映射/减少/任何操作(这不会提供任何性能好处)之外,延迟加载还有什么好处?

如果是这样,什么是现实世界的用例?


答案 1

您的终端操作 可能支持您的参数,因为它需要流的所有元素。toArray()

某些终端操作不会。对于这些,如果流没有被懒惰地执行,那将是一种浪费。举两个例子:

//example 1: print first element of 1000 after transformations
IntStream.range(0, 1000)
    .peek(System.out::println)
    .mapToObj(String::valueOf)
    .peek(System.out::println)
    .findFirst()
    .ifPresent(System.out::println);

//example 2: check if any value has an even key
boolean valid = records.
    .map(this::heavyConversion)
    .filter(this::checkWithWebService)
    .mapToInt(Record::getKey)
    .anyMatch(i -> i % 2 == 0)

第一个流将打印:

0
0
0

也就是说,中间操作将只在一个元素上运行。这是一个重要的优化。如果不是懒惰,那么所有的调用都必须在所有元素上运行(绝对没有必要,因为你只对一个元素感兴趣)。中间操作可能代价高昂(如第二个示例中所示)peek()

短路端子操作(不是)使这种优化成为可能。toArray


答案 2

懒惰对于API的用户非常有用,特别是当管道评估的最终结果可能非常大时!Stream

一个简单的例子是 Java API 本身中的 Files.lines 方法。如果您不想将整个文件读入内存,并且只需要第一行,则只需编写:N

Stream<String> stream = Files.lines(path); // lazy operation

List<String> result = stream.limit(N).collect(Collectors.toList()); // read and collect

推荐