36

私はすでにこれこのStream.skip質問を読みましたが、観測された動作がJDKの作成者によって意図されたものであるかどうかはまだ疑問です.

1..20 の数字を簡単に入力してみましょう:

List<Integer> input = IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

unordered()次に、並列ストリームを作成しskip()、さまざまな方法で を組み合わせて、結果を収集しましょう。

System.out.println("skip-skip-unordered-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .skip(1)
            .unordered()
            .collect(Collectors.toList()));
System.out.println("skip-unordered-skip-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .unordered()
            .skip(1)
            .collect(Collectors.toList()));
System.out.println("unordered-skip-skip-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .unordered()
            .skip(1)
            .skip(1)
            .collect(Collectors.toList()));

ここでは、フィルタリング ステップは基本的に何もしませんが、ストリーム エンジンの処理がさらに難しくなります。現在、出力の正確なサイズがわからないため、一部の最適化がオフになっています。次の結果があります。

skip-skip-unordered-toList: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
// absent values: 1, 2
skip-unordered-skip-toList: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20]
// absent values: 1, 15
unordered-skip-skip-toList: [1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20]
// absent values: 7, 18

結果はまったく問題なく、すべてが期待どおりに機能します。最初のケースでは、最初の 2 つの要素をスキップしてから、特定の順序でリストに収集するように依頼しました。2 番目のケースでは、最初の要素をスキップするように依頼し、次に unordered に変えて、もう 1 つの要素をスキップするように依頼しました (どの要素かは気にしません)。3 番目のケースでは、最初に順不同モードになり、次に任意の 2 つの要素をスキップしました。

1 つの要素をスキップして、非順序モードでカスタム コレクションに収集しましょう。カスタム コレクションは次のようになりますHashSet

System.out.println("skip-toCollection: "
        + input.parallelStream().filter(x -> x > 0)
        .skip(1)
        .unordered()
        .collect(Collectors.toCollection(HashSet::new)));

出力は満足のいくものです:

skip-toCollection: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
// 1 is skipped

したがって、一般に、ストリームが順序付けられている限りskip()、最初の要素をスキップし、それ以外の場合は任意の要素をスキップすることを期待しています。

ただし、同等の順序付けられていない端末操作を使用しましょうcollect(Collectors.toSet())

System.out.println("skip-toSet: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .unordered()
            .collect(Collectors.toSet()));

出力は次のようになります。

skip-toSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 20]
// 13 is skipped

他の順序付けられていない端末操作 ( 、 、 など) でも同じ結果が得forEachられfindAnyますanyMatch。この場合、ステップを削除unordered()しても何も変わりません。stepunordered()は現在の操作からストリームを正しく順不同にしますが、順不同の端末操作は、skip()使用された場合に結果に影響を与える可能性があるにもかかわらず、最初からストリーム全体を順不同にします。これは私にとって完全に誤解を招くようです: unordered コレクターを使用することは、端末操作の直前にストリームを unordered モードにして、同等の ordered コレクターを使用することと同じだと思います。

だから私の質問は:

  1. この動作は意図されたものですか、それともバグですか?
  2. はいの場合、それはどこかに文書化されていますか?Stream.skip()のドキュメントを読みました。順序付けられていない端末操作については何も述べていません。また、Characteristics.UNORDEREDのドキュメントはあまり理解されておらず、ストリーム全体で順序付けが失われるとは述べていません。最後に、パッケージ概要の注文セクションもこのケースをカバーしていません。おそらく私は何かが欠けていますか?
  3. 順序付けされていない端末操作によってストリーム全体が順序付けされていないことが意図されている場合、なぜunordered()step がこの時点以降だけ順序付けされないようにするのですか? この動作に頼ることができますか? それとも、最初のテストがうまく機能しただけで幸運でしたか?
4

2 に答える 2

30

ストリーム フラグ (ORDERED、SORTED、SIZED、DISTINCT) の目的は、不要な作業を回避する操作を可能にすることです。ストリーム フラグを含む最適化の例は次のとおりです。

  • ストリームが既にソートされていることがわかっている場合sorted()は、ノーオペレーションです。
  • ストリームのサイズがわかっている場合は、正しいサイズの配列を事前に割り当ててtoArray()、コピーを回避できます。
  • 入力に意味のある遭遇順序がないことがわかっている場合は、遭遇順序を維持するために追加の手順を実行する必要はありません。

パイプラインの各ステージには、一連のストリーム フラグがあります。中間操作では、ストリーム フラグを挿入、保持、またはクリアできます。たとえば、フィルタリングは、並べ替え/個別性を保持しますが、サイズを保持しません。マッピングはサイズを保持しますが、並べ替えや識別性は保持しません。並べ替えは、並べ替え性を注入します。すべての決定はローカルであるため、中間操作のフラグの扱いは非常に簡単です。

端末操作のフラグの扱いは、より微妙です。ORDERED は、端末操作に最も関連するフラグです。また、終端操作が UNORDERED である場合は、非順序性を逆伝播します。

なぜこれを行うのですか?さて、このパイプラインを考えてみましょう:

set.stream()
   .sorted()
   .forEach(System.out::println);

は順序どおりに動作するように制約されていないためforEach、リストを並べ替える作業は完全に無駄な作業です。limitしたがって、この最適化の機会を失わないように、この情報を ( などの短絡操作に到達するまで) 逆伝播します。同様に、distinct順序付けされていないストリームの最適化された実装を使用できます。

この動作は意図されたものですか、それともバグですか?

はい :) 逆伝播は意図的なものです。これは、誤った結果を生成しない便利な最適化であるためです。ただし、バグの部分は、以前skipの を超えて伝播していることです。これはすべきではありません。したがって、UNORDERED フラグの逆伝播は過度に攻撃的であり、これはバグです。バグを投稿します。

はいの場合、それはどこかに文書化されていますか?

実装の詳細にすぎません。正しく実装されていれば、気付かないでしょう (ストリームが高速であることを除いて)。

于 2015-06-18T12:58:44.010 に答える
1

@Ruben、あなたはおそらく私の質問を理解していません。大まかに問題は、unordered().collect(toCollection(HashSet::new)) が collect(toSet()) と異なる動作をする理由です。もちろん、 toSet() が順不同であることは知っています。

おそらくですが、とにかく、もう一度試してみます。

コレクターの toSet およびtoCollectionの Javadoc を見ると、toSetが順不同のコレクターを提供していることがわかります。

これは {@link Collector.Characteristics#UNORDERED unordered} コレクターです。

つまり、UNORDERED Characteristicを持つCollectorImplです。Collector.Characteristics#UNORDERED の Javadoc を見ると、次のように読むことができます。

コレクション操作が、入力要素の出現順序を維持することにコミットしていないことを示します

Collector の Javadoc では、次のことも確認できます。

並行コレクターの場合、実装はリダクションを並行して自由に実装できます (必須ではありません)。同時リダクションとは、アキュムレータ関数が複数のスレッドから同時に呼び出され、アキュムレーション中に結果を分離したままにするのではなく、同時に変更可能な同じ結果コンテナーを使用するリダクションです。コレクターが {@link Characteristics#UNORDERED} 特性を持っている場合、または元のデータが順序付けられていない場合にのみ、同時削減を適用する必要があります。

これは、UNORDERED特性を設定すると、ストリームの要素がアキュムレータに渡される順序をまったく気にしないため、パイプラインから要素を任意の順序で抽出できることを意味します。 .

ところで、例で unordered() を省略した場合、同じ動作が得られます。

    System.out.println("skip-toSet: "
            + input.parallelStream().filter(x -> x > 0)
                .skip(1)
                .collect(Collectors.toSet()));

さらに、Stream の skip() メソッドからヒントが得られます。

{@code skip()} は通常、順次ストリーム パイプラインでは安価な操作ですが、順序付けられた並列パイプラインでは非常に高価になる可能性があります。

順序付けされていないストリーム ソース ({@link #generate(Supplier)} など) を使用するか、{@link #unordered()} で順序付けの制約を削除すると、速度が大幅に向上する場合があります

使用時

Collectors.toCollection(HashSet::new)

通常の「順序付けられた」コレクター (UNORDERED 特性のないもの) を作成しています。私にとっては、順序付けを気にしていることを意味し、したがって、要素が順番に抽出され、期待される動作が得られます。

于 2015-06-17T05:49:28.053 に答える