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

时间:2015-03-24 09:46:54

标签: java lambda java-8 java-stream

我有以下示例代码:

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 s的第二种情况下,我们看到尽管找到满足过滤条件的第一个元素(它只是任何第一个元素,因为lambda总是返回true),流的其他内容仍然是通过过滤功能。

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

8 个答案:

答案 0 :(得分:53)

TL; DR,这已在JDK-8075939中解决,并已在Java 10中修复。

在查看实施(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);
                    }
                }
            };
        }
    };
}

推进一个项目的方法最终在子流上调用forEach而没有任何提前终止的可能性,flatMap方法开头的注释甚至告诉了这个缺席的特征。 / p>

因为这不仅仅是一个优化的事情,因为它暗示代码只是在子流无限时断开,我希望开发人员很快证明他们“可以做得比这更好”......


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

关于规范,大部分内容可以在

中找到

chapter “Stream operations and pipelines” of the package specification

  

...

     

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

     

...

     

...懒惰还允许在没有必要时避免检查所有数据;对于诸如“查找超过1000个字符的第一个字符串”之类的操作,只需要检查足够的字符串以找到具有所需特征的字符串,而不检查源中可用的所有字符串。 (当输入流是无限的而不仅仅是大的时候,这种行为变得更加重要。)

     

...

     

此外,某些操作被视为短路操作。如果在呈现无限输入时,它可能产生有限流,则中间操作是短路的。如果在呈现无限输入时它可以在有限时间内终止,则终端操作是短路的。在流水线中进行短路操作是处理无限流以在有限时间内正常终止的必要条件,但不是充分条件。

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

答案 1 :(得分:16)

输入流的元素一个接一个地懒散地消耗掉。第一个元素1由两个flatMap转换为流-1, 0, 1, 0, 1, 2, 1, 2, 3,因此整个流只对应于第一个输入元素。嵌套流由管道急切地实现,然后展平,然后馈送到filter阶段。这解释了你的输出。

上述内容并非源于一个基本限制,但它可能会使嵌套流的全面懒惰变得更加复杂。我怀疑让它变得更好是一个更大的挑战。相比之下,Clojure的懒惰seqs为每个这样的嵌套级别提供了另一层包装。由于这种设计,当嵌套运行到极端时,操作甚至可能会失败StackOverflowError

答案 2 :(得分:8)

关于无限子流的破坏,当一个人抛出中间(而不是终端)短路操作时,flatMap的行为变得更加令人惊讶。

虽然以下工作正常,但打印出无限的整数序列

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

以下代码仅打印&#34; 1&#34;,但仍然终止:

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

我无法想象读取那些不是错误的规范。

答案 3 :(得分:5)

在我的免费StreamEx库中,我介绍了短路收集器。当使用短路收集器(如MoreCollectors.first())收集顺序流时,从源中消耗一个元素。在内部,它以非常脏的方式实现:使用自定义异常来打破控制流。使用我的库,您的样本可以用这种方式重写:

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

答案 4 :(得分:1)

虽然 JDK-8075939 已在 Java 11 中得到修复并向后移植到 10 和 8u222,但仍然存在 flatMap() 在使用 Stream.iterator() 时并非真正懒惰的边缘情况:JDK-8267359 ,仍然存在于 Java 17 中。

这个

Iterator<Integer> it =
    Stream.of("a", "b")
        .flatMap(s -> Stream
            .of(1, 2, 3, 4)
            .filter(i -> { System.out.println(i); return true; }))
        .iterator();

it.hasNext(); // This consumes the entire flatmapped stream
it.next();

印刷品

1
2
3
4

虽然这个:

Iterator<Integer> it =
    Stream.of("a", "b")
        .flatMap(s -> Stream
            .iterate(1, i -> i)
            .filter(i -> { System.out.println(i); return true; }))
        .iterator();

it.hasNext();
it.next();

永不终止

答案 5 :(得分:0)

我同意其他人这是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的开发者。

答案 6 :(得分:0)

不幸的是.flatMap()并不懒惰。不过,此处提供了自定义flatMap解决方法:Why .flatMap() is so inefficient (non lazy) in java 8 and java 9

答案 7 :(得分:0)

今天我也偶然发现了这个错误。行为不那么严格,导致简单的情况(如下所示)可以正常工作,但是类似的生产代码不起作用。

 stream(spliterator).map(o -> o).flatMap(Stream::of).flatMap(Stream::of).findAny()

对于那些不能再等几年才能迁移到JDK-10的家伙,还有另一种真正的惰性流。它不支持并行。它专用于JavaScript翻译,但是对我来说可行,因为界面相同。

StreamHelper是基于集合的,但是很容易使用Spliterator。

https://github.com/yaitskov/j4ts/blob/stream/src/main/java/javaemul/internal/stream/StreamHelper.java