web-dev-qa-db-ja.com

Java Stream APIはループを置き換えることを目的としていますか?

私は次の意味での質問を意味します:コレクションでの単純なループの発生コード内のJava 8以上コードの臭いと見なされる(正当化された例外を除く)

Java 8になると、すべてのStream AP​​Iで今すぐ処理できると良いと思いました。特にparallelStream()を使用する場合は順序が問題ではないことがわかっている場合はどこでも、これにより、JVMはコードの実行を最適化することができます。

チームメイトはここでは違う考え方をします。彼らはラムダ変換は読みにくく、現在ストリームをあまり使用していません。コードフォーマッタが次のように記述するように強制すると、ストリームが読みにくくなることに同意します。

return projects.parallelStream().filter(lambda -> lambda.getGeneratorSource() != null)
        .flatMap(lambda -> lambda.getFolders().prallelStream().map(mu -> Pair.of(mu, lambda.getGeneratorSource())))
        .filter(lambda -> !lambda.getLeft().equals(lambda.getRight())).map(Pair::getLeft)
        .filter(lambda -> lambda.getDerivative().isPresent() || lambda.getDpi().isPresent()
                || lambda.getImageScale().isPresent() || lambda.getImageSize().isPresent())
        .findAny().isPresent();

私はむしろこれらをこのように書くことを好みます:

return projects.parallelStream()

// skip all projects that don’t declare a generator source
.filter(λ ->
    λ.getGeneratorSource() != null
)

/* We need to remove the folders which are the source folders of their
 * project. To do so, we create pairs of each folder with the source
 * folder … */
.flatMap(λ ->
    λ.getFolders().stream()
    .map(μ ->
        Pair.of(μ, λ.getGeneratorSource()) // Pair<Folder, Folder>
    )
)
// … and drop all folders that ARE the source folders
.filter(λ ->
    !λ.getLeft().equals(λ.getRight())
)
/* For the further processing, we only need the folders, so we can unbox
 * them now */
.map(Pair::getLeft)

// only look for folders that declare a method to generate images
.filter(λ ->
    λ.getDerivative().isPresent() || λ.getDpi().isPresent() ||
    λ.getImageScale().isPresent() || λ.getImageSize().isPresent()
)

// return whether there is any
.findAny().isPresent();

はい、returnが一番上にあり、外観は次のようにかなり異なります。

for (Project project : projects) {
    if (Objects.nonNull(project.getGeneratorSource())) {
        continue;
    }
    for (Folder folder : project.getFolders()) {
        if (folder.equals(project.getGeneratorSource())) {
            continue;
        } else if (folder.getDerivative().isPresent() || folder.getDpi().isPresent()
                || folder.getImageScale().isPresent() || folder.getImageSize().isPresent()) {
            return true;
        }
    }
}
return false;

しかし、それは単なる習慣の問題ではありませんか?したがって、私の質問は評価の問題です。

Streamは使用する基本的なものである必要があります(Java初心者はforループをすべて最初に学習するか、最初にストリームの使用方法を学習する必要があります)、- orは時期尚早の最適化であり、コードがボトルネックとなっていることが示された後でストリームを使用する必要があります。

(意見に基づくものではない:現代のJava教科書(ITトレーニングにはまだ本があるのですか?)

編集1:質問がフォーカスを失うのを防ぐために:私の質問は次のとおりですIs Streamは、基本的なプログラミングパラダイムとしてJava =プログラマは頻繁に、または上級ソフトウェアエンハンサーのパフォーマンス機能として使用する必要がありますが、回避する必要がありますか?内部で確認することもできます。JVMを使用できますか決定ストリーム構築を使用するか、オブジェクトを順次処理するか(たとえば、ホットスポットを検出した場合にのみそれを実行するか?)、またはする必要があるか複数のスレッドを使用して毎回バックグラウンドで複雑な並列化ロジックを設定し、これにより多くのオーバーヘッドが発生する可能性がありますか?2番目のケースでは、Stream allおよびどこにでも。

1
Matthias Ronge

Streamよりもforsを優先する理由は、forがすべてを行うであるためです。数十行にまたがるパターンを認識する必要がなく、操作ごとに異なる名前を使用できます。

両方のバージョンの問題は、1つの方法ですべてを実行することです。あなたのような機能がありません

Stream<T> <T> notNull(Stream<T> objects) { 
    return objects.filter(o -> o != null); 
}

Stream<Pair<Folder, Folder>> getFolderPairs(Project project) { 
    return project.getFolders().stream()
        .map(folder ->
            Pair.of(folder, project.getGeneratorSource())
        );
}

boolean isNotSourceFolder(Pair<Folder, Folder> folders) {
    return !folders.getLeft().equals(folders.getRight());
}

Stream<Folder> subfoldersFor(Stream<Project> projects) { 
    return notNull(projects)
        .flatMap(getFolderPairs)
        .filter(isNotSourceFolder)
        .map(Pair::getLeft)
}

boolean canGenerateImages(Folder folder) { 
    return (folder.getDerivative().isPresent() 
         || folder.getDpi().isPresent() 
         || folder.getImageScale().isPresent() 
         || folder.getImageSize().isPresent()) 
}

どこでもlambdaではなく、ラムダ式のパラメーターに意味のある名前を使用していることに注意してください。

これらを使用すると、元の関数は、物事を一緒に必要とするフォーマッターであっても、はるかに単純になります。

return subfoldersFor(projects.parallelStream())
    .anyMatch(canGenerateImages);
3
Caleth

Streamは、Java=プログラマーが頻繁に使用する必要がある基本的なプログラミングパラダイムとして、または上級ソフトウェアエンハンサーのパフォーマンス機能として意図されているので、避ける必要がありますか?

あなたが見ていない別のオプションがあると思います。 Streamsの主な利点(控えめな見方では)は、並列および並列アルゴリズムのパラダイムであり、正しく理解するのがはるかに簡単です。

つまり、一般的にループを置き換えることを意図したものではありません(これはやや人気のある意見ですが)。しかし、それは実際には専門家だけを対象としたものでもありません。これは、平均的な開発者が以前は専門家しか管理できなかったアプローチを実装できるようにすることを目的としています。

この時点まで、多くの人が並列ストリームを使用してマルチスレッドを実装します。これは問題なく、最適なソリューションにつながる可能性もありますが、実際には既存の同時実行ライブラリよりもはるかに洗練されていません。そして、私は単にコードを書くのが簡単だという意味ではありません。つまり、同時実行ライブラリの機能は、ストリームAPIが提供するものを超えて、非常に多くの設計オプションを提供します。問題は、同時実行性、Javaメモリモデル、および広範なライブラリについての理解を深める必要があることです。

したがって、最終的には、ストリームを高度なオプションと見なすのではなく、初心者向けに設計されたものと見なします。

そうは言っても、ストリームAPIには、forループと同等の機能よりも優れた機能がたくさんあります。マッピングとフィルタリングはよりクリーンで、再利用オプションがはるかに多くなっています。 ifステートメントがループに埋め込まれている。残念ながら、Javaは、デフォルトでストリームとループを一緒に機能させません。 その理由があります しかし、「ストリームまたはbust 'とIMOは、多くの状況でコードの可読性を低下させます。

2
JimmyJames

ストリームはJavaエコシステムに追加されますが、従来のforループを置き換えることはできません。

囲んでいる変数への制限付きアクセス

主な制限は、ラムダ内のコードが、囲んでいるメソッドの変数に完全にアクセスできないことです。そして、ラムダは実際には匿名関数クラスのインスタンスの省略形であるため、それを克服することはできません。ラムダ式のコードは、技術的には囲みスコープではなく、完全に異なるクラスにあります。

読みやすさ

私はストリームベースのコードを読みにくいものに関連付けようとしていますが、その欠点は主にすべてのストリーム操作を1つの巨大な式にチェーンする習慣に起因しています。

従来の非ストリームプログラミングでは、このような多くのメソッド呼び出しの連鎖がコードのにおいであると見なしますが、ストリームを使用すると、コード(行)を占める式を幸福(?)に記述できます。

これらの式をわかりやすい部分に分割して、中間ストリームにローカル変数として意味のある名前を付け、変数の宣言でストリームタイプを確認できるようにしてみませんか?または、いくつかのストリーム操作を意味のある名前のメソッドにグループ化しますか?ストリーム式を単一のモノリシックブロックとして記述する必要はありません。

その理由はおそらく、ほとんどのチュートリアルがその方向に私たちを導き、管理可能な部分に分割されたストリーム式を表示しないことです。

デバッグ

そして、デバッグがあります。ストリームベースのコードはデバッグがはるかに困難です。

  • 実際の実行は、オペレーションチェーンの最後の収集ステップによってトリガーされます。コードをシングルステップ実行することは事実上不可能です。
  • スタックトレースはコード構造に似ていません。囲んでいるメソッドとストリーム操作内のビジネスロジックの間には、多くの合成メソッド呼び出しがあります。これは、デバッグセッションだけでなく、運用時のエラー報告にも影響します。高く評価されたスタックトレースロギングは、その価値の多くを失います。

パフォーマンス

そして最後に、パフォーマンスの問題があります。最良のオプティマイザを使用しても、多数の個別の機能インターフェイスインスタンスをチェーン化するオーバーヘッドには代償があります。それはほとんどの状況で重要ではないかもしれませんが、それはあります。

結論

可読性の問題は別のコーディングスタイルを採用することで克服できますが、欠点が残っているため、ストリームは従来のforループを適切に置き換えることができません。

1
Ralf Kleberhoff