为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

九州编程 2021-11-03 14:32:18
我最近阅读了很多关于 Java 8 流的文章,还有几篇关于使用 Java 8 流进行延迟加载的文章:here and over here。我似乎无法摆脱延迟加载完全没用的感觉(或者充其量只是提供零性能价值的次要语法便利)。我们以这段代码为例: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 operation在这种情况下toArray()调用的是 ,而我们的流是惰性的,仅在调用终端操作时才执行。当然我也可以这样做:  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>) 创建无限流并且提供的供应商函数逐渐从远程服务器接收数据的情况。在这种情况下,服务器调用只会在需要时在终端操作中进行。不处理陈旧数据?什么?延迟加载如何防止某人处理陈旧数据?TLDR:除了能够在以后运行 filter/map/reduce/whatever 操作(这提供零性能优势)之外,延迟加载还有什么好处吗?如果是这样,现实世界的用例是什么?
查看完整描述

3 回答

?
Helenr

TA贡献1780条经验 获得超3个赞

您的终端操作 ,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不是)使这种优化成为可能。


查看完整回答
反对 回复 2021-11-03
?
陪伴而非守候

TA贡献1757条经验 获得超8个赞

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


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


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


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


查看完整回答
反对 回复 2021-11-03
?
湖上湖

TA贡献2003条经验 获得超2个赞

我有一个来自我们代码库的真实示例,因为我将对其进行简化,但不完全确定您可能喜欢它还是完全掌握它...

我们有一个需要 的服务List<CustomService>,我想称之为。现在为了调用它,我要去一个数据库(比现实简单得多)并获得一个List<DBObject>; 为了从中获得a List<CustomService>,需要进行一些重大的转换。

这是我的选择,就地转换并传递列表。很简单,但可能不是那么理想。第二种选择,重构服务,接受 aList<DBObject>和 a Function<DBObject, CustomService>。这听起来微不足道,但它使懒惰(除其他外)成为可能。该服务有时可能只需要该 List 中的几个元素,或者有时需要max某个属性等 - 因此我不需要对所有元素进行大量转换,这就是Stream API基于拉的懒惰是赢家的地方。

在 Streams 出现之前,我们曾经使用guava. 它也Lists.transform( list, function)很懒惰。

这不是流的基本特征,即使没有番石榴也可以完成,但这样要简单得多。这里提供的例子findFirst很好,而且最容易理解;这就是懒惰的全部意义所在,元素仅在需要时才被拉取,它们不会从一个中间操作以块的形式传递到另一个操作,而是一次从一个阶段传递到另一个阶段。


查看完整回答
反对 回复 2021-11-03
  • 3 回答
  • 0 关注
  • 154 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信