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

时间:2017-12-19 14:10:49

标签: java lambda java-stream java-9

我正在创建使用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

ExpectedOutput:

Sample1
Sample2
Sample3

期望的原因是takeWhile应该执行直到内部条件变为真。我还在flatmap中添加了printout语句以进行调试。流返回两次,符合预期。

但是,如果链中没有flatmap,这样就可以了。

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 水槽::接受

@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);
    }
}

4 个答案:

答案 0 :(得分:54)

这是JDK 9中的一个错误 - 来自issue #8193856

  

takeWhile错误地认为上游操作支持并尊重取消,遗憾的是flatMap并非如此。

解释

如果订购了流,takeWhile应显示预期的行为。在您的代码中并非完全如此,因为您使用forEach来放弃订单。如果你关心它,你在这个例子中做了,你应该使用forEachOrdered。有趣的是:这并没有改变任何事情。

所以也许首先没有订购流? (在这种情况下the behavior is ok。)如果为从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. 如果这是最后一个元素则停止
  4. 看起来很有前途,对吧?

    没有flatMap

    在好的情况下&#34; (没有flatMap;您的第二个示例)forEachWithCancel直接在WhileOp sink上运行,您可以看到它是如何发挥作用的:

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

    耶!

    使用flatMap

    在&#34;坏情况下&#34; (使用flatMap;您的第一个示例),forEachWithCancelflatMap操作进行操作,但只需调用forEachRemaining上的ArraySpliterator {"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操作,但从不检查是否已取消。因此,它将热切地穿过那个&#34;子流&#34;中的所有元素。在停止之前,甚至可能through the rest of the stream

答案 1 :(得分:20)

这个 是一个错误,无论我如何看待它 - 感谢Holger的评论。我不想把这个答案放在这里(严肃地说!),但没有一个答案清楚地说明这是一个错误。

人们说这必须是有序/无序的,这不是真的,因为这会报告true 3次:

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引入的取消标记。

答案 2 :(得分:11)

如果你看the documentation for takeWhile

  

如果订购此流,则[返回]包含该流的流   从此流中获取的与给定匹配的元素的最长前缀   谓语。

     

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

您的信息流已巧合订购,但takeWhile 不知道。因此,它返回第二个条件 - 子集。您的takeWhile表现得像filter

如果您在takeWhile之前添加对sorted的来电,则会看到您期望的结果:

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

答案 3 :(得分:9)

原因是flatMap操作也是intermediate operations,其中(一个) 有状态短路中间操作 使用takeWhile

Holger在this answer中所指出的flatMap的行为无疑是一个不应错过的参考,以便了解此类短路操作的意外输出。

通过引入终端操作来确定性地使用有序流并对样本执行以下操作,可以通过拆分这两个中间操作来实现预期结果:

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接受进一步跟踪此错误。