与平面图不同的工作方式

我正在用 take while 创建片段来探索它的可能性。当与 latMap 一起使用时,行为与预期不一致。请找到下面的代码片段。

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

预期的原因是 take while 应该一直执行,直到内部条件变为 true。我还在 latmap 中添加了打印输出语句,以便进行调试。流只返回两次,这与期望值内联。

但是,在链中不使用平面映射时,这样做就可以很好地工作。

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

实际输出:

Sample3

这里,实际输出与预期输出相匹配。

免责声明: 这些代码片段只是用于代码练习,不适用于任何有效的用例。

更新: Bug 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);
}
}
4696 次浏览

原因是 flatMap操作也是一个 中间作业,使用 有状态短路中间操作 takeWhile中的一个。

Holger 在 这个答案中指出的 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中作为 bug 进一步跟踪。

如果你看看 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));

这是 JDK 9中的一个 bug ——来自 第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直接操作 WhileOp作为 sink,你可以看到这是如何发挥:

  • ReferencePipeline::forEachWithCancel循环播放:
    • 给出了每个流元素的 WhileOps::accept
    • 在每个元素之后查询 WhileOps::cancellationRequested
  • 在某个时刻,"Sample4"使谓词失败,流被取消

耶!

flatMap

在“坏情况”(使用 flatMap; 第一个示例)中,forEachWithCancel操作 flatMap操作,该操作简单地在 ArraySpliterator上为 {"Sample3", "Sample4", "Sample5"}调用 forEachRemaining{"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操作 但从不检查它是否被取消。因此,在停止之前,它将急切地遍历该“子流”中的所有元素,甚至可能是 穿过小溪的其余部分

这个 是一个错误,无论我怎么看它-并感谢你霍格尔的意见。我不想把这个答案放在这里(真的!)但是没有一个答案清楚地说明这是一个错误。

人们说,这必须与有序/无序,这是不正确的,因为这将报告 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引入。