takeWhile()与Flatmap的工作方式不同


75

我正在用takeWhile创建片段,以探索其可能性。当与flatMap结合使用时,其行为与预期不符。请在下面找到代码片段。

String[][] strArray = {{"Sample1", "Sample2"}, {"Sample3", "Sample4", "Sample5"}};

Arrays.stream(strArray)
        .flatMap(indStream -> Arrays.stream(indStream))
        .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
        .forEach(ele -> System.out.println(ele));

实际输出:

Sample1
Sample2
Sample3
Sample5

预期输出:

Sample1
Sample2
Sample3

期望的原因是takeWhile应该一直执行到内部条件变为真为止。我还在平面图中添加了打印输出语句以进行调试。流仅返回两次,这与期望一致。

但是,这在链中没有平面图的情况下也很好用。

String[] strArraySingle = {"Sample3", "Sample4", "Sample5"};
Arrays.stream(strArraySingle)
        .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
        .forEach(ele -> System.out.println(ele));

实际输出:

Sample3

此处,实际输出与预期输出匹配。

免责声明:这些摘录仅用于代码实践,不能用于任何有效的用例。

更新: 错误JDK-8193856:该修补程序将作为JDK 10的一部分提供。更改将更正whileOps Sink :: accept

@Override 
public void accept(T t) {
    if (take = predicate.test(t)) {
        downstream.accept(t);
    }
}

更改的实现:

@Override
public void accept(T t) {
    if (take && (take = predicate.test(t))) {
        downstream.accept(t);
    }
}

Answers:


54

这是JDK 9中的错误-从问题#8193856开始

takeWhile错误地假设上游操作支持并接受取消操作,不幸的是并非如此flatMap

说明

如果已订购流,takeWhile则应显示预期的行为。在您的代码中,情况并非完全如此,因为您使用forEach会放弃订购。如果您关心它(在本示例中这样做),则应该使用它forEachOrdered。有趣的是:那没有任何改变。🤔

因此,也许流不是一开始就订购的?(在这种情况下,行为可以。)如果为从其创建的流创建一个临时变量,strArray并通过((StatefulOp) stream).isOrdered();在断点处执行表达式来检查其是否有序,则您会发现它确实是有序的:

String[][] strArray = {{"Sample1", "Sample2"}, {"Sample3", "Sample4", "Sample5"}};

Stream<String> stream = Arrays.stream(strArray)
        .flatMap(indStream -> Arrays.stream(indStream))
        .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"));

// breakpoint here
System.out.println(stream);

这意味着这很可能是实现错误。

融入守则

正如其他人所怀疑的那样,我现在也认为这可能flatMap渴望有关。更准确地说,这两个问题可能具有相同的根本原因。

查看的来源WhileOps,我们可以看到以下方法:

@Override
public void accept(T t) {
    if (take = predicate.test(t)) {
        downstream.accept(t);
    }
}

@Override
public boolean cancellationRequested() {
    return !take || downstream.cancellationRequested();
}

此代码用于takeWhile检查给定的流元素t是否predicate满足:

  • 如果是这样,它将元素传递给downstream操作,在这种情况下为System.out::println
  • 如果不是,则将其设置take为false,因此在下次询问是否应取消管道(即完成)时,它将返回true

这涵盖了takeWhile操作。您需要知道的另一件事是forEachOrdered导致终端操作执行该方法ReferencePipeline::forEachWithCancel

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

所有这些是:

  1. 检查管道是否被取消
  2. 如果没有,则将水槽前进一个元素
  3. 如果这是最后一个元素,则停止

看起来很有前途吧?

不带 flatMap

在“好的情况下”(没有flatMap第二个示例);forEachWithCancel直接在WhileOpas上运行sink,您可以看到它是如何进行的:

  • ReferencePipeline::forEachWithCancel 进行循环:
    • WhileOps::accept 被赋予每个流元素
    • WhileOps::cancellationRequested 在每个元素之后被查询
  • 在某个时候"Sample4"使谓词失败,流被取消

好极了!

flatMap

在“最坏情况”(与flatMap您的第一个例子),forEachWithCancel运行在flatMap运行,虽然,这只是调用forEachRemainingArraySpliterator进行{"Sample3", "Sample4", "Sample5"},这将会:

if ((a = array).length >= (hi = fence) &&
    (i = index) >= 0 && i < (index = hi)) {
    do { action.accept((T)a[i]); } while (++i < hi);
}

忽略所有这些hifence东西,只有在将数组处理拆分为并行流时才使用,这是一个简单的for循环,它将每个元素传递给takeWhile操作,但从不检查它是否被取消。因此,它将在停止之前热切地遍历“子流”中的所有元素,甚至可能遍历流的其余部分


17
@Eugene:好吧,我敢打赌它已经连接到这个了。碰巧适用于端子短路操作,因为它们忽略了多余的元素,但是现在我们有中间短路操作……因此,这实际上是个好消息,因为这意味着现在有更多的压力来修复该错误(性能低劣或当子流无限时中断是远远不够的)…
Holger

10
它不会遍历整个流。如果子流的最后一个元素与谓词匹配,则外部流的取消支持将起作用,例如,String[][] strArray = { {"Sample1", "Sample2"}, {"Sample3", "Sample4"}, {"Sample5", "Sample6"}, };用作输入,并且看起来起作用。如果只有中间元素匹配,则flatMap对取消的无知会导致标志被后续元素的评估覆盖。
Holger

@Holger我只是说“子流”(我的措辞不清楚),甚至没有考虑跟随“子流”。更改了措辞,并从澄清中链接到您的评论。
Nicolai Parlog

16
似乎,他们已经听到您的声音了:bugs.openjdk.java.net/browse/JDK-8193856
Stefan Zobel

20

无论我怎么看,这都是一个错误-谢谢Holger的评论。我不想在这里(严重!)放这个答案,但是没有一个答案明确指出这是一个错误。

人们说这必须是有序/无序的,这是不正确的,因为它将报告true3次:

Stream<String[]> s1 = Arrays.stream(strArray);
System.out.println(s1.spliterator().hasCharacteristics(Spliterator.ORDERED));

Stream<String> s2 = Arrays.stream(strArray)
            .flatMap(indStream -> Arrays.stream(indStream));
System.out.println(s2.spliterator().hasCharacteristics(Spliterator.ORDERED));

Stream<String> s3 = Arrays.stream(strArray)
            .flatMap(indStream -> Arrays.stream(indStream))
            .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"));
System.out.println(s3.spliterator().hasCharacteristics(Spliterator.ORDERED));

同样有趣的是,如果将其更改为:

String[][] strArray = { 
         { "Sample1", "Sample2" }, 
         { "Sample3", "Sample5", "Sample4" }, // Sample4 is the last one here
         { "Sample7", "Sample8" } 
};

然后Sample7Sample8不会成为输出的一部分,否则会。似乎flatmap 忽略了由引入的取消标志dropWhile


11

如果您查看以下文档takeWhile

如果此流是有序的,则[返回]一个流,该流由从该流中获取的,与给定谓词匹配的元素的最长前缀组成。

如果此流是无序的,则[返回]一个由从该流中获取的与给定谓词匹配的元素子集组成的流。

您的信息流是偶然排序的,但takeWhile 不知道是这样。这样,它将返回第二个条件-子集。你takeWhile的举止像个傻瓜filter

如果您将通话添加到sortedbefore takeWhile,则会看到预期的结果:

Arrays.stream(strArray)
      .flatMap(indStream -> Arrays.stream(indStream))
      .sorted()
      .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
      .forEach(ele -> System.out.println(ele));

17
为什么不下令,或者为什么不知道它呢?有序流的“连接”应该有序,不是吗?
JB Nizet

9
@JBNizet,但是如果您分别执行每个步骤Stream<String[]> s1 = Arrays.stream(strArray); System.out.println(s1.spliterator().hasCharacteristics(Split‌​erator.ORDERED)),依此类推-它们都会产生一个ORDERED流,这似乎是一个尚未报告的错误
Eugene

8
我所看到的@Michael(根据先前的评论)-您的结论对我来说是错误的
尤金(Eugene)

10
但是takeWhile不知道它是什么”……好吧,为什么订购流及其子流时却不知道它,为什么还.sorted().unordered() .takeWhile(…)继续做正确的事呢?我会说,是因为sorted有状态操作会缓冲整个输入,然后进行真正的延迟迭代。
Holger

2
“您的流是偶然排序的,但是takeWhile不知道它是什么。因此,它返回第二个条件-子集。您的takeWhile就像一个过滤器。”:但这听起来确实是错误的。如果未对流进行排序,它将以某些不可预测的顺序返回其元素。现在,takeWhile应按接收到的顺序对它实际接收到的元素进行操作,并在元素不满足其谓词时立即停止。如果要过滤无序流,则应使用filter
乔治

9

这样做的原因是该flatMap操作也是中间操作,与之一起使用状态短路中间操作 (之一)takeWhile

flatMap正如Holger在此答案中指出的那样,无疑是一种参考,可以理解这种短路操作的意外输出。

通过引入两个终端操作来确定性地进一步使用有序流并将它们作为示例执行,可以通过拆分这两个中间操作来实现您的预​​期结果:

List<String> sampleList = Arrays.stream(strArray).flatMap(Arrays::stream).collect(Collectors.toList());
sampleList.stream().takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
            .forEach(System.out::println);

另外,似乎还有一个相关的Bug#JDK-8075939可以跟踪已经注册的此行为。

编辑:可以在JDK-8193856处将其作为错误进行进一步跟踪。


8
我不明白你的解释。在我看来,这种行为似乎是一个错误。您建议的替代方案需要两个Stream管道,这可能不太理想。
伊兰(Eran)

2
@Eran实际上,该行为似乎是一个错误。建议的替代方法是将终端操作引入完成(排气)flatMap操作,然后处理要执行的流takeWhile
纳曼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.