いつストリームを使用する必要がありますか?


99

Listとそのstream()方法を使用しているときに質問を見つけました。私が知っている間どのようにそれらを使用するために、私は約かなりよく分からないときにそれらを使用します。

たとえば、さまざまな場所へのさまざまなパスを含むリストがあります。ここで、与えられた単一のパスに、リストで指定されたパスが含まれているかどうかを確認したいと思います。boolean条件が満たされたかどうかに基づいてを返したいのですが。

もちろん、これ自体は難しい作業ではありません。しかし、ストリームを使用する必要があるのか​​、それともfor(-each)ループを使用する必要があるのか​​と思います。

リスト

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

例-ストリーム

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

例-For-Eachループ

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

pathパラメータは常に小文字であることに注意してください。

私の最初の推測は、条件が満たされればループがすぐに戻るため、for-eachアプローチの方が速いということです。一方、ストリームはフィルタリングを完了するためにすべてのリストエントリをループします。

私の仮定は正しいですか?もしそうなら、なぜ(あるいは、むしろとき)私が使用することになりstream()、その後?


11
ストリームは、従来のforループよりも表現力があり、読みやすくなっています。後で、if-thenや条件などの組み込みに注意する必要があります。ストリーム式は非常に明確です。ファイル名を小文字に変換し、何かでフィルタリングしてから、カウント、収集などを行います。結果は非常に反復的です計算の流れの表現。
ジャンバティストユネス2017

12
ここは必要ありませんnew String[]{…}。そのまま使用Arrays.asList("my/path/one", "my/path/two")
Holger

4
ソースがの場合、String[]を呼び出す必要はありませんArrays.asList。を使用して配列をストリーミングすることができますArrays.stream(array)。ちなみに、isExcludedテストの目的がまったくわかりません。の要素EXCLUDE_PATHSが文字通りパス内のどこかに含まれているかどうかは本当に興味深いですか?すなわち、同様にisExcluded("my/path/one/foo/bar/baz")戻ります…trueisExcluded("foo/bar/baz/my/path/one/")
Holger

3
グレートは、私が知らなかったArrays.stream、それを指摘して感謝の方法。実際、私が投稿した例は、私以外の誰にとってもまったく役に立たないようです。私はの動作に注意だisExcluded方法が、それは本当に私は自分のために必要なだけの何か、したがって、あなたの質問に答えるために:はい、それはスコープの中に収まらないように私は、言及しないように希望の理由で興味深いものです元の質問の。
mcuenez 2017

1
なぜtoLowerCase小文字の定数に適用されるのですか?それはpath議論に適用されるべきではないのですか?
Sebastian Redl 2017

回答:


78

あなたの仮定は正しいです。ストリームの実装はforループよりも低速です。

ただし、このストリームの使用はforループと同じくらい高速でなければなりません。

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

これはアイテムを反復処理し、アイテムにString::toLowerCase1つずつ適用してフィルターを適用し、一致する最初のアイテム終了します。

collect()anyMatch()は両方とも端末操作です。anyMatch()ただし、最初に見つかったアイテムで終了しますが、collect()すべてのアイテムを処理する必要があります。


2
素晴らしい、findFirst()との組み合わせについては知りませんでしたfilter()。どうやら思ったほどストリームの使い方がわかりませ
mcuenez 2017

4
ストリームAPIのパフォーマンスに関するいくつかの本当に興味深いブログ記事とプレゼンテーションがWebにあります。これらは、内部でこの機能がどのように機能するかを理解するのに非常に役立ちました。もし興味があれば、少しだけ研究することをお勧めします。
Stefan Pries 2017

編集後、他の回答のコメントで私の質問にも回答したので、あなたの回答は受け入れられるべきものだと思います。しかし、私は@ rvit34にコードを投稿したことでクレジットを付与したいと思います:-)
mcuenez

34

Streamsを使用するかどうかの決定は、パフォーマンスを考慮したものではなく、読みやすさによって決定されます。パフォーマンスに関しては、他にも考慮すべき点があります。

あなたの.filter(path::contains).collect(Collectors.toList()).size() > 0アプローチでは、すべての要素を処理Listし、サイズを比較する前にそれらを一時的なに収集しますが、2つの要素で構成されるストリームの場合、これはほとんど問題になりません。

を使用.map(String::toLowerCase).anyMatch(path::contains)すると、要素の数が大幅に多い場合に、CPUサイクルとメモリを節約できます。それでも、これStringは一致が見つかるまで、それぞれを小文字の表現に変換します。明らかに、使用にはポイントがあります

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

代わりに。したがって、を呼び出すたびに小文字への変換を繰り返す必要はありませんisExcludedEXCLUDE_PATHS文字列の要素数または文字列の長さが本当に大きくなる場合は、

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

LITERALフラグを使用して文字列を正規表現パターンとしてコンパイルすると、通常の文字列操作と同じように動作しますが、実際の比較に関しては、エンジンが準備にある程度の時間を費やすことができます。

もちろん、これは、準備に費やされた時間を補うのに十分な後続のテストがある場合にのみ効果があります。これが当てはまるかどうかを判断することは、この操作がパフォーマンスにまったく影響を与えるかどうかという最初の質問に加えて、実際のパフォーマンスに関する考慮事項の1つです。ストリームとforループのどちらを使用するかは問題ではありません。

ちなみに、上記のコード例は元のコードのロジックを保持しているので、私には疑問です。あなたのisExcludedメソッドが返すtrue、それが返されますので、指定されたパスは、リストの要素のいずれかが含まれている場合trueのため/some/prefix/to/my/path/oneだけでなく、my/path/one/and/some/suffixあるいは/some/prefix/to/my/path/one/and/some/suffix

それも文字列なdummy/path/onerousのでcontains、基準を満たしていると見なされますmy/path/one


可能なパフォーマンス最適化に関する素晴らしい洞察、ありがとう。 あなたの答えの最後の部分について:あなたのコメントへの私の返答が満足のいくものではなかった場合、実際のコードではなく、他の人が私が求めていることを理解するための単なるヘルパーとして私のサンプルコードを検討してください。また、より良い例がある場合は、いつでも質問を編集できます。
mcuenez 2017

3
この操作は本当にやりたいことなので、変更する必要はありません。私は、最後のセクションを将来の読者のために残しておきます。そのため、これは一般的な操作ではなく、すでに説明されており、コメントを追加する必要がないことを認識しています…
Holger

実際には、作業メモリの量がサーバーの制限を超えている場合、ストリームはメモリの最適化に最適です。
ColacX

21

うん。あなたが正しいです。ストリームアプローチにはある程度のオーバーヘッドがあります。ただし、次のような構造を使用できます。

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

ストリームを使用する主な理由は、ストリームを使用することでコードがよりシンプルで読みやすくなるためです。


3
あるanyMatchのショートカットfilter(...).findFirst().isPresent()
mcuenez

6
はい、そうです!それは私の最初の提案よりも優れています。
Stefan Pries 2017

8

Javaでのストリームの目標は、並列コードの記述の複雑さを単純化することです。関数型プログラミングに触発されています。シリアルストリームは、コードをよりクリーンにするためのものです。

パフォーマンスが必要な場合は、設計されたparallelStreamを使用する必要があります。一般的に、シリアルのものは遅いです。

読むべき良い記事があります ForLoopStreamおよびParallelStreamパフォーマンス

コードでは、終了メソッドを使用して、最初の一致で検索を停止できます。(anyMatch ...)


5
小さなストリームの場合、および他のいくつかのケースでは、起動コストのために並列ストリームが遅くなる可能性があることに注意してください。また、順序付けされていない並列化可能なオペレーションではなく、順序付けされたターミナルオペレーションがある場合は、最後に再同期します。
97

0

他の人が多くの良い点を述べたので、私はストリーム評価の遅延評価について述べたいだけです。私たちが行うとmap()下ケースパスのストリームを作成するために、我々は代わりにストリームがされ、すぐに全体の流れを作成していないいい加減に構築された性能は、ループの伝統と同等であるべき理由です。これは、完全なスキャンをやっていない、map()そしてanyMatch()同時に実行されています。いったんanyMatch()戻って真の、それは短絡となります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.