web-dev-qa-db-ja.com

takeWhile()はflatmapで異なる動作をします

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ステートメントを追加しました。ストリームは2回だけ返されますが、これは期待どおりです。

ただし、これはチェーン内にフラットマップがなくても正常に機能します。

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

これはJDK 9のバグです- issue#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);

これは、これが実装エラーである可能性が非常に高いことを意味します。

コードに

他の人が疑っているように、私は今、このmightflatMapに熱心に接続されていると考えています。より正確には、両方の問題の根本原因が同じである可能性があります。

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操作を対象としています。もう1つ知っておく必要があるのは、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. そうでない場合は、シンクを1要素だけ進めます
  3. これが最後の要素である場合は停止します

有望そうですね。

flatMapなし

flatMapなし」の2番目の例である「良いケース」では、forEachWithCancelWhileOpsinkとして直接操作し、これがどのように再生されるかを確認できます。

  • ReferencePipeline::forEachWithCancelはループを実行します:
    • WhileOps::acceptには各ストリーム要素が与えられます
    • WhileOps::cancellationRequestedは各要素の後に照会されます
  • ある時点で"Sample4"は述語に失敗し、ストリームはキャンセルされます

わーい!

flatMapを使用

「悪い場合」(flatMapの場合、最初の例)では、forEachWithCancelflatMap操作で動作しますが、{"Sample3", "Sample4", "Sample5"}forEachRemainingArraySpliteratorを呼び出すだけです。

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

配列処理が並列ストリーム用に分割されている場合にのみ使用されるhiおよびfenceスタッフをすべて無視すると、これは単純なforループであり、各要素をtakeWhile操作に渡しますキャンセルされたかどうかを確認します。したがって、停止する前に、その「サブストリーム」内のすべての要素を熱心に通過します。おそらく、 ストリームの残りの部分を通して です。

54
Nicolai

これはis私がそれをどう見てもバグです-あなたのコメントをホルガーに感謝します。私はこの答えをここに入れたくありませんでしたが、これがバグであることを明確に述べている答えはありません。

これは順序付き/順序なしである必要があると言われていますが、これは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" } 
};

Sample7およびSample8は出力の一部ではありません。それ以外の場合は出力されます。 flatmapignoresは、dropWhileによって導入されるキャンセルフラグのようです。

20
Eugene

takeWhileのドキュメント を見ると:

このストリームが順序付けされている場合、指定された述語に一致する、このストリームから取得された要素の最長のプレフィックスで構成されるストリームを返します。

このストリームが順序付けられていない場合、このストリームから取得され、指定された述語に一致する要素のサブセットで構成されるストリームを返します。

ストリームは偶然に順序付けられていますが、takeWhileはそれが分かっていません。そのため、2番目の条件(サブセット)を返しています。 takeWhileは、 filter のように動作しています。

sortedの前に takeWhile への呼び出しを追加すると、期待する結果が表示されます。

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

その理由は、 flatMap 操作も 中間操作 であり、これにより、ステートフル短絡中間操作takeWhile が使用されます。

この回答 でHolgerが指摘したflatMapの動作は、このような短絡操作の予期しない出力を理解するために見逃してはならない参照です。

ターミナルオペレーションを導入してこれら2つの中間オペレーションを分割し、順序付けされたストリームをさらに決定的に使用し、サンプルに対して次のように実行することで、期待どおりの結果を得ることができます。

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 があるようです。この動作は既に登録されています。

Edit:これは JDK-8193856 で受け入れられますバグ。

9
Naman