为什么flatMap()之后的filter()在Javastream中“不完全”懒惰?

我有以下示例代码:

System.out.println( "Result: " + Stream.of(1, 2, 3) .filter(i -> { System.out.println(i); return true; }) .findFirst() .get() ); System.out.println("-----------"); System.out.println( "Result: " + Stream.of(1, 2, 3) .flatMap(i -> Stream.of(i - 1, i, i + 1)) .flatMap(i -> Stream.of(i - 1, i, i + 1)) .filter(i -> { System.out.println(i); return true; }) .findFirst() .get() ); 

输出如下:

 1 Result: 1 ----------- -1 0 1 0 1 2 1 2 3 Result: -1 

从这里我看到,在第一种情况下, stream实际上是懒惰的 – 我们使用findFirst()所以一旦我们有第一个元素,我们的过滤lambda不会被调用。 然而,在使用flatMap的第二种情况下,我们看到尽pipe发现了满足过滤条件的第一个元素(它只是任何第一个元素,因为lambda总是返回true),但是stream的更多内容仍然通过过滤函数馈送。

我试图理解为什么它的行为是这样的,而不是像在第一种情况下计算第一个元素之后放弃。 任何有用的信息将不胜感激。

当查看实现( ReferencePipeline.java )时,我们看到方法[ link ]

 @Override final void forEachWithCancel(Spliterator<P_OUT> spliterator, Sink<P_OUT> sink) { do { } while (!sink.cancellationRequested() && spliterator.tryAdvance(sink)); } 

将调用findFirst操作。 要特别注意的是sink.cancellationRequested() ,它允许在第一次匹配时结束循环。 与[ link ]比较

 @Override public final <R> Stream<R> flatMap(Function<? super P_OUT, ? extends Stream<? extends R>> mapper) { Objects.requireNonNull(mapper); // We can do better than this, by polling cancellationRequested when stream is infinite return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT | StreamOpFlag.NOT_SIZED) { @Override Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) { return new Sink.ChainedReference<P_OUT, R>(sink) { @Override public void begin(long size) { downstream.begin(-1); } @Override public void accept(P_OUT u) { try (Stream<? extends R> result = mapper.apply(u)) { // We can do better that this too; optimize for depth=0 case and just grab spliterator and forEach it if (result != null) result.sequential().forEach(downstream); } } }; } }; } 

用于推进一个项目的方法最终在子stream上调用forEach而没有任何可能的提前终止,并且在flatMap方法的开始处的注释甚至告诉这个缺less的特征。

因为这不仅仅是一个优化的事情,因为它暗示了当子stream是无限的时候,代码简单地被破坏,我希望开发人员很快certificate他们“可以做得比这更好”…


为了说明这个含义,当Stream.iterate(0, i->i+1).findFirst()按预期工作时, Stream.of("").flatMap(x->Stream.iterate(0, i->i+1)).findFirst()将以无限循环结束。

关于规范,大部分可以在这里find

包规范的“stream操作和stream水线”一章 :

中间操作返回一个新的stream。 他们总是懒惰的 ;

懒惰也可以避免在没有必要的时候检查所有的数据; 对于“查找长度超过1000个字符的第一个string”等操作,只需要检查足够的string就可以find具有所需特征的string,而不必检查源中可用的所有string。 (当inputstream是无限的而不仅仅是大的时候,这种行为变得更加重要。)

此外,有些操作被认为是短路操作。 如果在呈现无限input的情况下中间操作是短路的,则可能产生有限的stream。 terminal操作是短路的,如果在呈现无限input的情况下,它可能在有限的时间内终止。 在stream水线中进行短路操作是处理无限stream在有限时间内正常终止的必要但不充分的条件。

显而易见的是,短路操作不能保证有限的时间终止,例如当filter不匹配处理无法完成的任何项目时,而是通过简单地忽略不支持在有限时间内终止的实现操作的短路性质远不符合规范。

inputstream的元素被逐一消耗掉。 第一个元素1由两个flatMap转换为streamflatMap ,使得整个stream只对应于第一个input元素。 嵌套stream被pipe道急切地物化,然后变平坦,然后被馈送到filter阶段。 这解释了你的输出。

以上内容并不是来自一个根本性的限制,但它可能会让事情变得复杂得多。 我怀疑要做到这一点将是一个更大的挑战。 为了比较,Clojure的懒惰seqs为每个这样的嵌套层次获得了另一层包装。 由于这种devise,当嵌套被执行到极限时,操作甚至可能因StackOverflowError而失败。

关于无限子stream的断裂,当投入中间 (而不是terminal)短路操作时,flatMap的行为更加令人惊讶。

以下虽然按预期工作,打印出无限的整数序列

 Stream.of("x").flatMap(_x -> Stream.iterate(1, i -> i + 1)).forEach(System.out::println); 

以下代码只打印出“1”,但仍不会终止:

 Stream.of("x").flatMap(_x -> Stream.iterate(1, i -> i + 1)).limit(1).forEach(System.out::println); 

我无法想象一个阅读规范,其中不是一个错误。

在我的免费StreamEx库中,我介绍了短路收集器。 在收集具有短路收集器的顺序stream(如MoreCollectors.first() )时,只有一个元素从源中消耗。 在内部,它的实现方式非常肮脏:使用自定义exception来中断控制stream。 使用我的库,您的示例可以用这种方式重写:

 System.out.println( "Result: " + StreamEx.of(1, 2, 3) .flatMap(i -> Stream.of(i - 1, i, i + 1)) .flatMap(i -> Stream.of(i - 1, i, i + 1)) .filter(i -> { System.out.println(i); return true; }) .collect(MoreCollectors.first()) .get() ); 

结果如下:

 -1 Result: -1 

我同意其他人这是在JDK-8075939打开的错误。 而且由于这一年还没有确定。 我想推荐你: AbacusUtil

 N.println("Result: " + Stream.of(1, 2, 3).peek(N::println).first().get()); N.println("-----------"); N.println("Result: " + Stream.of(1, 2, 3) .flatMap(i -> Stream.of(i - 1, i, i + 1)) .flatMap(i -> Stream.of(i - 1, i, i + 1)) .peek(N::println).first().get()); // output: // 1 // Result: 1 // ----------- // -1 // Result: -1 

披露:我是AbacusUtil的开发者。

不幸的是.flatMap()不是懒惰的。 但是,自定义的flatMap解决方法可以在这里find: 为什么在java 8和java 9中.flatMap()效率很低(非懒惰)