518

次のうち、Java 8 でより適切な方法はどれですか?

Java 8:

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

Java7:

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

ラムダで「簡略化」できる for ループがたくさんありますが、それらを使用する利点は本当にありますか? パフォーマンスと可読性が向上しますか?

編集

また、この質問をより長い方法に拡張します。ラムダから親関数を返したり壊したりできないことは知っています。これらを比較する際には、これも考慮に入れる必要がありますが、他に考慮すべきことはありますか?

4

8 に答える 8

592

を使用することをお勧めしますfor-eachKeep It Simple, Stupidの原則に違反するだけでなく、初心者forEach()には少なくとも次のような欠点があります。

  • 最終変数以外は使用できません。したがって、次のようなコードは forEach ラムダに変換できません。
Object prev = null;
for(Object curr : list)
{
    if( prev != null )
        foo(prev, curr);
    prev = curr;
}
  • チェックされた例外を処理できません。ラムダは、チェックされた例外をスローすることを実際に禁止されていませんが、一般的な機能インターフェイスはConsumer何も宣言しません。したがって、チェック例外をスローするコードは、それらをtry-catchまたはでラップする必要がありますThrowables.propagate()。しかし、そうしても、スローされた例外がどうなるかは必ずしも明確ではありません。腸のどこかで飲み込んでしまう可能性がありますforEach()

  • 限られたフロー制御returnラムダの Aはcontinuefor-each の a と同じですが、 a に相当するものはありませんbreak。また、戻り値、短絡、またはフラグの設定などを行うのも困難です (最終変数以外のルールに違反していなければ、少しは緩和されたはずです)。「これは単なる最適化ではありませんが、一部のシーケンス (ファイル内の行の読み取りなど) に副作用があるか、無限のシーケンスがある可能性があることを考慮すると重要です。」

  • これは、最適化が必要なコードの 0.1% を除いて、恐ろしい、恐ろしいことです。並列コードはすべて熟考する必要があります (ロック、揮発性、および従来のマルチスレッド実行の特に厄介な側面を使用しない場合でも)。バグを見つけるのは難しいでしょう。

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

  • 並列処理が必要な場合は、 ExecutorService を使用する方がおそらくはるかに高速であり、それほど難しくありません。ストリームは自動 (読み取り: 問題についてあまり知らない)であり、特殊な (読み取り: 一般的なケースでは非効率的) 並列化戦略 ( fork-join 再帰的分解) を使用します。

  • ネストされた呼び出し階層と、神が禁じている並列実行のために、デバッグがより混乱します。デバッガーは、周囲のコードから変数を表示する際に問題が発生する可能性があり、ステップスルーなどは期待どおりに機能しない可能性があります。

  • 一般に、ストリームはコーディング、読み取り、およびデバッグがより困難です。実際、これは複雑な「流れるような」API 全般に当てはまります。複雑な単一ステートメント、ジェネリックの多用、および中間変数の欠如の組み合わせにより、混乱を招くエラー メッセージが生成され、デバッグが妨げられます。「このメソッドには型 X のオーバーロードがありません」という代わりに、「どこかで型を台無しにしましたが、どこで、どのようにかわからない」というようなエラー メッセージが表示されます。同様に、コードが複数のステートメントに分割され、中間値が変数に保存される場合ほど簡単に、デバッガーでステップ実行して物事を調べることはできません。最後に、コードを読んで、実行の各段階での型と動作を理解することは、簡単ではないかもしれません。

  • 痛い親指のように突き出ています。Java 言語にはすでに for-each ステートメントがあります。なぜそれを関数呼び出しに置き換えるのですか? 式のどこかに副作用を隠すことを奨励するのはなぜですか? なぜ扱いにくいワンライナーを奨励するのですか? 通常の for-each と新しい forEach を意のままに混在させるのは悪いスタイルです。コードはイディオム (繰り返しのためにすぐに理解できるパターン) で話す必要があり、使用するイディオムが少ないほど、コードが明確になり、どのイディオムを使用するかを決定するのに費やす時間が少なくなります (私のような完璧主義者にとっては大きな時間の浪費です! )。

ご覧のとおり、私は forEach() が理にかなっている場合を除いて、あまり好きではありません。

私にとって特に不快なのは、(実際には method があるにもかかわらず)Stream実装せず、 for-each では使用できず、 forEach() でのみ使用できるという事実です。Streams を Iterables にキャストすることをお勧めします。より良い代替手段は、実装を含む多くの Stream API の問題を修正するStreamExを使用することです。Iterableiterator(Iterable<T>)stream::iteratorIterable

つまりforEach()、次の場合に役立ちます。

  • 同期されたリストを原子的に反復します。これ以前は、 で生成されたリストは、Collections.synchronizedList()get や set などに関してアトミックでしたが、反復するときはスレッドセーフではありませんでした。

  • 並列実行 (適切な並列ストリームを使用)。これにより、問題が Streams および Spliterators に組み込まれているパフォーマンスの前提条件と一致する場合、ExecutorService を使用する場合に比べて数行のコードを節約できます。

  • 同期されたリストのように、反復を制御することで恩恵を受ける特定のコンテナー(ただし、人々がより多くの例を持ち出すことができない限り、これは主に理論上のものです)

  • forEach()メソッド参照引数 (つまり)を使用して、単一の関数をよりきれいに呼び出すlist.forEach (obj::someMethod)。ただし、チェックされた例外、より困難なデバッグ、およびコードを記述する際に使用するイディオムの数の削減に関するポイントに留意してください。

参考にした記事:

EDIT:ラムダの元の提案(http://www.javac.info/closures-v06a.html Google Cacheなど)のいくつかは、私が言及した問題のいくつかを解決したようです(もちろん、独自の複雑さを追加しながら)。

于 2013-11-24T16:45:31.013 に答える
173

この利点は、操作を並行して実行できる場合に考慮されます。( http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - 内部および外部反復に関するセクションを参照)

  • 私の観点からの主な利点は、ループ内で実行されることの実装を、並列で実行するか順次実行するかを決定することなく定義できることです。

  • ループを並行して実行したい場合は、単純に書くことができます

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

    スレッド処理などのために追加のコードを書く必要があります。

注:私の答えでは、java.util.Streamインターフェースを実装する結合を想定しました。joins がjava.util.Iterableインターフェースのみを実装する場合、これはもはや当てはまりません。

于 2013-05-19T14:05:46.320 に答える
122

この質問を読むIterable#forEachと、ラムダ式と組み合わせることで、従来の for-each ループを作成するためのショートカット/代替であるという印象を受けることができます。これは単に真実ではありません。OPからのこのコード:

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

書くためのショートカットとして意図されていません

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

このように使用するべきではありません。代わりに、書き込みのショートカットとして意図されています (まったく同じではありませんが)。

joins.forEach(new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
});

これは、次の Java 7 コードの代わりになります。

final Consumer<T> c = new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
};
for (T t : joins) {
    c.accept(t);
}

上記の例のように、ループの本体を機能的なインターフェイスに置き換えると、コードがより明確になります。つまり、(1) ループの本体は周囲のコードと制御フローに影響を与えず、(2)ループの本体は、周囲のコードに影響を与えることなく、関数の別の実装に置き換えることができます。外部スコープの非最終変数にアクセスできないことは、関数/ラムダの欠陥ではなく、従来の for-each ループのセマンティクスとセマンティクスを区別する機能です。Iterable#forEachの構文にIterable#forEach慣れると、コードに関する追加情報がすぐに得られるため、コードが読みやすくなります。

従来の for-each ループは、Java では(「ベスト プラクティス」という乱用された用語を避けるため)良い慣例であり続けるでしょう。しかし、これは悪い習慣や悪いスタイルと見なされるべきだという意味ではありません。仕事をするために適切なツールを使用することは、常に良い習慣です。Iterable#forEachIterable#forEach

のマイナス面はIterable#forEachこのスレッドで既に議論されているため、おそらく を使用する理由をいくつか示しますIterable#forEach

  • コードをより明示的にするには:前述のように、状況によっては、コードをより明示的で読みやすいものにするIterable#forEach ことができます。

  • コードをより拡張可能で保守しやすくするには:ループの本体として関数を使用すると、この関数を別の実装に置き換えることができます (戦略パターンを参照)。たとえば、サブクラスによって上書きされる可能性のあるメソッド呼び出しでラムダ式を簡単に置き換えることができます。

    joins.forEach(getJoinStrategy());
    

    次に、機能インターフェースを実装する列挙型を使用してデフォルト戦略を提供できます。これにより、コードの拡張性が向上するだけでなく、ループの実装がループ宣言から分離されるため、保守性も向上します。

  • コードをよりデバッグしやすくするには: 宣言からループ実装を分離すると、デバッグがより簡単になります。これは、メイン コードを .xml で混乱させることなく、デバッグ メッセージを出力する特殊なデバッグ実装を使用できるためですif(DEBUG)System.out.println()。デバッグ実装は、たとえば、実際の関数実装を装飾するデリゲートにすることができます。

  • パフォーマンスが重要なコードを最適化するには:このスレッドのいくつかの主張に反して、少なくとも ArrayList を使用し、「-client」モードで Hotspot を実行している場合、従来の for-each ループよりも優れたパフォーマンスを既に提供Iterable#forEach しています。このパフォーマンスの向上は小さく、ほとんどのユース ケースでは無視できますが、この追加のパフォーマンスが違いを生む場合があります。たとえば、ライブラリのメンテナーは、既存のループ実装の一部をIterable#forEach.

    この声明を事実で裏付けるために、私はCaliperでいくつかのマイクロベンチマークを行いました。テスト コードは次のとおりです (git の最新の Caliper が必要です)。

    @VmOptions("-server")
    public class Java8IterationBenchmarks {
    
        public static class TestObject {
            public int result;
        }
    
        public @Param({"100", "10000"}) int elementCount;
    
        ArrayList<TestObject> list;
        TestObject[] array;
    
        @BeforeExperiment
        public void setup(){
            list = new ArrayList<>(elementCount);
            for (int i = 0; i < elementCount; i++) {
                list.add(new TestObject());
            }
            array = list.toArray(new TestObject[list.size()]);
        }
    
        @Benchmark
        public void timeTraditionalForEach(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : list) {
                    t.result++;
                }
            }
            return;
        }
    
        @Benchmark
        public void timeForEachAnonymousClass(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(new Consumer<TestObject>() {
                    @Override
                    public void accept(TestObject t) {
                        t.result++;
                    }
                });
            }
            return;
        }
    
        @Benchmark
        public void timeForEachLambda(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(t -> t.result++);
            }
            return;
        }
    
        @Benchmark
        public void timeForEachOverArray(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : array) {
                    t.result++;
                }
            }
        }
    }
    

    結果は次のとおりです。

    「-client」を指定して実行するとIterable#forEach、ArrayList に対する従来の for ループよりもパフォーマンスが高くなりますが、配列を直接反復するよりも低速です。「-server」で実行すると、すべてのアプローチのパフォーマンスはほぼ同じになります。

  • 並列実行のためのオプションのサポートを提供するには:ここで既に述べたように、ストリームIterable#forEachを使用して並列に機能インターフェースを実行できることは確かに重要な側面です。はループが実際に並列に実行されることを保証しないため、これはオプションの機能と見なす必要があります。を使用してリストを反復処理することにより、次のように明示的に言います。このループは並列実行をサポートしていますが、それに依存していません。繰り返しますが、これは欠点ではなく機能です。Collection#parallelStream()list.parallelStream().forEach(...);

    並列実行の決定を実際のループの実装から遠ざけることで、コード自体に影響を与えることなく、オプションでコードを最適化できます。これは良いことです。また、デフォルトの並列ストリームの実装がニーズに合わない場合でも、独自の実装を提供することを誰も妨げていません。たとえば、基盤となるオペレーティング システム、コレクションのサイズ、コア数、およびいくつかの設定に応じて、最適化されたコレクションを提供できます。

    public abstract class MyOptimizedCollection<E> implements Collection<E>{
        private enum OperatingSystem{
            LINUX, WINDOWS, ANDROID
        }
        private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
        private int numberOfCores = Runtime.getRuntime().availableProcessors();
        private Collection<E> delegate;
    
        @Override
        public Stream<E> parallelStream() {
            if (!System.getProperty("parallelSupport").equals("true")) {
                return this.delegate.stream();
            }
            switch (operatingSystem) {
                case WINDOWS:
                    if (numberOfCores > 3 && delegate.size() > 10000) {
                        return this.delegate.parallelStream();
                    }else{
                        return this.delegate.stream();
                    }
                case LINUX:
                    return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator());
                case ANDROID:
                default:
                    return this.delegate.stream();
            }
        }
    }
    

    ここでの良い点は、ループの実装がこれらの詳細を知ったり気にしたりする必要がないことです。

于 2014-03-19T10:02:30.007 に答える
13

forEach()for-each ループよりも高速に実装できます。これは、イテラブルが、標準の反復子の方法とは対照的に、その要素を反復する最良の方法を知っているためです。したがって、違いは内部でループするか、外部でループするかです。

たとえば、次のArrayList.forEach(action)ように単純に実装できます。

for(int i=0; i<size; i++)
    action.accept(elements[i])

多くの足場を必要とする for-each ループとは対照的に

Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    do something with `next`

ただし、 を使用して 2 つのオーバーヘッド コストを考慮する必要もありますforEach()。1 つはラムダ オブジェクトの作成であり、もう 1 つはラムダ メソッドの呼び出しです。それらはおそらく重要ではありません。

さまざまなユース ケースの内部/外部反復の比較については、 http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/も参照してください。

于 2013-05-19T18:00:07.113 に答える
10

TL;DR :List.stream().forEach()最速でした。

ベンチマークの反復からの結果を追加する必要があると感じました。私は非常に単純なアプローチ (ベンチマーク フレームワークなし) を採用し、5 つの異なる方法をベンチマークしました。

  1. クラシックfor
  2. 古典的な foreach
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

テスト手順とパラメータ

private List<Integer> list;
private final int size = 1_000_000;

public MyClass(){
    list = new ArrayList<>();
    Random rand = new Random();
    for (int i = 0; i < size; ++i) {
        list.add(rand.nextInt(size * 50));
    }    
}
private void doIt(Integer i) {
    i *= 2; //so it won't get JITed out
}

このクラスのリストは繰り返し処理されdoIt(Integer i)、毎回異なるメソッドを介してすべてのメンバーに適用されます。Main クラスでは、テスト済みのメソッドを 3 回実行して、JVM をウォームアップします。次に、テスト メソッドを 1000 回実行し、各反復メソッドにかかる時間を合計します ( を使用System.nanoTime())。それが終わったら、その合計を 1000 で割ります。それが結果の平均時間です。例:

myClass.fored();
myClass.fored();
myClass.fored();
for (int i = 0; i < reps; ++i) {
    begin = System.nanoTime();
    myClass.fored();
    end = System.nanoTime();
    nanoSum += end - begin;
}
System.out.println(nanoSum / reps);

Javaバージョン1.8.0_05を使用して、i5 4コアCPUでこれを実行しました

クラシックfor

for(int i = 0, l = list.size(); i < l; ++i) {
    doIt(list.get(i));
}

実行時間: 4.21 ミリ秒

古典的な foreach

for(Integer i : list) {
    doIt(i);
}

実行時間: 5.95 ミリ秒

List.forEach()

list.forEach((i) -> doIt(i));

実行時間: 3.11 ミリ秒

List.stream().forEach()

list.stream().forEach((i) -> doIt(i));

実行時間: 2.79 ミリ秒

List.parallelStream().forEach

list.parallelStream().forEach((i) -> doIt(i));

実行時間: 3.6 ミリ秒

于 2014-09-15T19:39:18.597 に答える
8

コメントをもう少し伸ばす必要があると感じています...

パラダイム\スタイルについて

それはおそらく最も注目に値する側面です。FPは、副作用を避けて手に入れることができるため、人気がありました。これは質問とは関係がないため、これから得られる長所と短所については深く掘り下げません。

ただし、 Iterable.forEach を使用した反復は FP に触発されたものであり、むしろより多くの FP を Java にもたらした結果であると言えます (皮肉なことに、純粋な FP では forEach はあまり使用されません。副作用)。

結局のところ、あなたが現在書いているのは、むしろ好み\スタイル\パラダイムの問題だと思います.

平行度について。

パフォーマンスの観点からは、foreach(...) よりも Iterable.forEach を使用することによる顕著な利点は約束されていません。

Iterable.forEach の公式ドキュメントによると:

すべての要素が処理されるか、アクションが例外をスローするまで、反復時に要素が発生する順序で、Iterable のコンテンツに対して指定されたアクションを実行します。

...つまり、暗黙の並列処理が存在しないことはかなり明確です。1 つ追加すると、LSP 違反になります。

現在、Java 8で約束されている「並列コレクション」がありますが、それらを使用するには、より明示的に使用する必要があり、それらを使用するために特別な注意を払う必要があります(たとえば、mschenk74の回答を参照)。

ところで: この場合、 Stream.forEachが使用されますが、実際の作業が並行して行われることは保証されません (基になるコレクションによって異なります)。

更新:一見するとそれほど明白ではなく、少し引き伸ばされているかもしれませんが、スタイルと読みやすさの観点には別の側面があります。

まず第一に、単純な古い forloops は単純で古いものです。誰もがすでにそれらを知っています。

2 つ目は、さらに重要なことです。おそらく、Iterable.forEach をワンライナー ラムダでのみ使用することをお勧めします。「本文」が重くなると、読みにくくなる傾向があります。ここから 2 つのオプションがあります - 内部クラス (yuck) を使用するか、単純な古い forloop を使用します。同じコードベースでさまざまな vays/styles が行われている同じこと (コレクションに対する反復) を見ると、人々はしばしばイライラしますが、これは事実のようです。

繰り返しますが、これは問題になる場合とそうでない場合があります。コードに取り組んでいる人々に依存します。

于 2013-05-19T16:31:47.590 に答える