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);
}
}
これは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);
これは、これが実装エラーである可能性が非常に高いことを意味します。
他の人が疑っているように、私は今、このmightが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
操作を対象としています。もう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;
}
これはすべて:
有望そうですね。
flatMap
なし「flatMap
なし」の2番目の例である「良いケース」では、forEachWithCancel
はWhileOp
をsink
として直接操作し、これがどのように再生されるかを確認できます。
ReferencePipeline::forEachWithCancel
はループを実行します:WhileOps::accept
には各ストリーム要素が与えられますWhileOps::cancellationRequested
は各要素の後に照会されます"Sample4"
は述語に失敗し、ストリームはキャンセルされますわーい!
flatMap
を使用「悪い場合」(flatMap
の場合、最初の例)では、forEachWithCancel
はflatMap
操作で動作しますが、{"Sample3", "Sample4", "Sample5"}
のforEachRemaining
でArraySpliterator
を呼び出すだけです。
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
操作に渡しますキャンセルされたかどうかを確認します。したがって、停止する前に、その「サブストリーム」内のすべての要素を熱心に通過します。おそらく、 ストリームの残りの部分を通して です。
これは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
は出力の一部ではありません。それ以外の場合は出力されます。 flatmap
ignoresは、dropWhile
によって導入されるキャンセルフラグのようです。
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));
その理由は、 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 で受け入れられますバグ。