1. ホーム
  2. java

[解決済み] Java 8 Iterable.forEach() vs foreachループ

2022-03-19 11:47:29

質問

Java 8では、次のうちどれがより良いプラクティスでしょうか?

Java 8です。

joins.forEach(join -> mIrc.join(mSession, join));

Java 7です。

for (String join : joins) {
    mIrc.join(mSession, join);
}

ラムダで簡略化できるforループがたくさんありますが、ラムダを使うメリットは本当にあるのでしょうか?パフォーマンスや可読性を向上させることができるのでしょうか?

EDIT

また、この質問を長いメソッドに拡張してみます。ラムダから親関数を返したりブレークしたりできないのは知っていて、これも比較する際に考慮すべきですが、他に何かありますか?

どのように解決するのですか?

より良い方法は for-each . に違反することに加え シンプルに、バカに の原則に反して、この新型の forEach() は、少なくとも次のような欠陥がある。

  • 非終端変数が使用できない . そのため、以下のようなコードはforEach lambdaにすることができません。
Object prev = null;
for(Object curr : list)
{
    if( prev != null )
        foo(prev, curr);
    prev = curr;
}

  • チェックした例外を処理できない . ラムダは実際にはチェックされた例外を投げることを禁じられているわけではありません。 Consumer は宣言していません。したがって、検査される例外を投げるコードはすべて try-catch または Throwables.propagate() . しかし、そうしたとしても、スローされた例外がどうなるかは、必ずしも明らかではありません。の中のどこかに飲み込まれるかもしれません。 forEach()

  • フロー制御の制限 . A return はラムダと同じです。 continue に相当するものはありませんが、for-eachでは break . また、戻り値やショートサーキット、あるいは セットフラグ (の違反でなければ、少しは緩和されたかもしれません)。 非終端変数なし のルールに従ってください)。 これは単なる最適化ではなく、(ファイルの行を読むような)いくつかのシーケンスに副作用があったり、無限シーケンスを持つ可能性があることを考慮すると、非常に重要です。

  • 並列に実行される可能性がある これは、最適化が必要な0.1%のコード以外には、とても恐ろしいことなのです。並列コードはすべて考え抜かなければなりません(ロックやボラタイルなど、従来のマルチスレッド実行の特に厄介な部分を使用しないとしても)。どんなバグでも見つけるのは難しいでしょう。

  • パフォーマンスを低下させる可能性がある なぜなら、JITはforEach()+lambdaをプレーンなループと同じ程度には最適化できないからで、特にlambdaが新しくなった現在ではそうです。最適化とは、ラムダを呼び出すオーバーヘッド(これは小さい)ではなく、最新のJITコンパイラが実行中のコードに対して行う高度な解析と変換のことを指しています。

  • 並列処理が必要な場合は、ExecutorService を使用する方がはるかに高速で、難易度もそれほど高くありません。 . ストリームは自動化されている(つまり、あなたの問題についてあまり知らない)。 は、特殊な (一般的なケースでは非効率的な) 並列化戦略を使用します ( フォークジョイン再帰的分解 ).

  • デバッグをより混乱させる なぜなら、ネストされたコール階層と、禁じられた並列実行があるからです。デバッガは周囲のコードから変数を表示することができず、ステップスルーのようなものが期待通りに動作しない可能性があります。

  • 一般的にストリームは、コーディング、読み込み、デバッグが難しい . 実はこれ、複雑な"にも言えることなんです。 流暢 API全般に言えることです。複雑な単一ステートメント、ジェネリックの多用、中間変数の不足などの組み合わせが、混乱したエラーメッセージを生み出し、デバッグを挫折させるのです。このメソッドにはX型のオーバーロードがありません」ではなく、「どこかで型を間違えたようですが、どこでどのように間違えたのかわかりません」というようなエラーメッセージが表示されます。最後に、コードを読んで、実行の各段階での型や動作を理解することは、自明ではないかもしれません。

  • 目立つ . Java言語には、すでにfor-each文があります。なぜそれを関数呼び出しに置き換えるのでしょうか?なぜ式のどこかに副作用を隠すことを推奨するのでしょうか?なぜ扱いにくいワンライナーを推奨するのでしょうか?通常のfor-eachと新しいfor-eachを無造作に混ぜるのは悪いスタイルです。コードはイディオム(繰り返しのために理解が早いパターン)で話すべきで、イディオムの数が少ないほどコードは明確になり、どのイディオムを使うかを決めるのに費やす時間が少なくなります(私のような完璧主義者には大きな時間の浪費です!)。

このように、私はforEach()が意味のある場合を除き、あまり好きではありません。

特に私が不快に感じるのは Stream は実装されていません。 Iterable (実際にはメソッドを持つにもかかわらず iterator ) で、for-each では使用できず、forEach() でしか使用できません。でストリームをイテラブルにキャストすることをお勧めします。 (Iterable<T>)stream::iterator . より良い代替案は StreamEx を実装するなど、Stream API の多くの問題を修正しています。 Iterable .

それはそうと。 forEach() は、以下のような場合に有効です。

  • 同期リストに対するアトミックな反復処理 . これ以前は Collections.synchronizedList() は、get や set のようなものに関してはアトミックでしたが、繰り返し実行する際にはスレッドセーフではありませんでした。

  • 並列実行(適切な並列ストリームを使用) . これは、ExecutorService を使用した場合と比較して、数行のコードを節約できます。もし、あなたの問題が Streams や Spliterators に組み込まれた性能の想定と一致するのであれば、この方法を使用することができます。

  • 特定のコンテナ 同期リストのように、イテレーションを制御することで利益を得ることができます (ただし、もっと多くの例を挙げてもらわない限り、これはほとんど理論上のことです)。

  • 単一の関数をよりクリーンに呼び出す を使用することで forEach() とメソッド参照の引数(つまり list.forEach (obj::someMethod) ). しかし、チェックされた例外、より困難なデバッグ、コードを書くときに使用するイディオムの数を減らすという点には留意してください。

参考にした記事

EDITです。 ラムダに関する当初の提案のいくつか(例えば http://www.javac.info/closures-v06a.html Googleキャッシュ ) は、私が述べた問題のいくつかを解決してくれました(もちろん、独自の複雑さはありますが)。