web-dev-qa-db-ja.com

Java 8のIterable.forEach()とforeachのループ

次のうちどれがJava 8でより良い習慣ですか?

Java 8:

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

Java 7:

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

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

_編集_

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

417
nebkat

より良い方法は、for-eachを使用することです。 Keep 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はfor-eachのcontinueと同じですが、breakに相当するものはありません。また、戻り値、短絡、またはフラグの設定のようなことを行うことは困難です(非最終変数なしルール)。 「これは単なる最適化ではなく、一部のシーケンス(ファイル内の行の読み取りなど)に副作用があるか、無限のシーケンスがある可能性があることを考慮すると重要です。」

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

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

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

  • ネストされた呼び出し階層と、並列実行の禁止により、デバッグがより複雑になります。デバッガーは、周囲のコードからの変数の表示に問題がある場合があり、ステップスルーなどが期待どおりに機能しないことがあります。

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

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

ご覧のとおり、私はforEach()の大ファンではありません。

特に不快なのは、StreamIterableを実装していない(実際にはiteratorメソッドを持っているにもかかわらず)、forEach()でのみfor-eachで使用できないという事実です。 (Iterable<T>)stream::iteratorでストリームをIterablesにキャストすることをお勧めします。より良い代替方法は StreamEx を使用することです。これにより、Iterableの実装など、Stream APIの多くの問題が修正されます。

ただし、forEach()は次の場合に役立ちます。

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

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

  • 同期リストのように、特定のコンテナは、反復の制御から恩恵を受けます(ただし、これは、人々がより多くの例を提示できない限り、大部分は理論的です)

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

参考に使用した記事:

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

465

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

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

  • ループを並列に実行したい場合は、単純に次のように書くことができます。

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

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

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

168
mschenk74

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

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

notは書き込みのショートカットとして意図されています

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

確かにこの方法で使用すべきではありません。代わりに、それは書くためのショートカットとして意図されています(ただし、notとまったく同じです)

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のセマンティクスを区別するfeatureです。 。 Iterable#forEachの構文に慣れると、コードに関する追加情報をすぐに取得できるため、コードが読みやすくなります。

従来のfor-eachループは、Javaでグッドプラクティス(使い古された用語 " ベストプラクティス "を避けるため)に確実にとどまります。しかし、これは、Iterable#forEachが悪い習慣または悪いスタイルと見なされるべきであることを意味しません。適切なツールを使用してジョブを実行することは常に良い習慣です。これには、従来のfor-eachループとIterable#forEachの混合も含まれます。

Iterable#forEachの欠点については既にこのスレッドで説明しているので、ここにいくつかの理由と、おそらくIterable#forEachを使用する理由を示します。

  • コードをより明確にする:上記のように、Iterable#forEachcanは、状況によってコードをより明確で読みやすくします。

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

    joins.forEach(getJoinStrategy());
    

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

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

  • パフォーマンスクリティカルなコードを最適化するには:このスレッドの一部のアサーションに反して、Iterable#forEachdoesは、従来のfor-eachループよりも優れたパフォーマンスを既に提供しています。少なくともArrayListを使用し、ホットスポットを「-client」モードで実行している場合。このパフォーマンスの向上はほとんどのユースケースでは小さく、無視できる程度ですが、この追加のパフォーマンスが違いを生む状況があります。例えば。既存のループ実装の一部を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」で実行すると、すべてのアプローチのパフォーマンスはほぼ同じです。

  • 並列実行のオプションのサポートを提供するために:ここですでに述べたように、 streams を使用してIterable#forEachの機能インターフェイスを並列に実行する可能性は確かに重要な側面です。 Collection#parallelStream()はループが実際に並列に実行されることを保証しないため、これをoptional機能と見なす必要があります。 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();
            }
        }
    }
    

    ここでの良いところは、ループの実装がこれらの詳細を知る必要がないことです。

101
Balder

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`

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

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

12
ZhongYu

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で、i 5 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ミリ秒

7
Assaf

コメントを少し拡張する必要があると思います...

paradigm\styleについて

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

ただし、Iterable.forEachを使用した反復はFPに触発され、むしろFPをJavaにもたらした結果と言えます(皮肉なことに、純粋なFPでは、副作用を導入する以外に何もしないため、forEachの使用はあまりないということです)。

最後に、それはむしろあなたが現在書いているテイスト\スタイル\パラダイムの問題であると言います。

並列処理について

パフォーマンスの観点からは、foreach(...)を介してIterable.forEachを使用することによる顕著なメリットはありません。

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

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

...つまり、暗黙的な並列処理が行われないことを文書で明確に示しています。 1つ追加するとLSP違反になります。

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

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

PDATE:はそれほど明白ではなく、一目見ただけでは少し伸びているかもしれませんが、スタイルと読みやすさの観点の別の側面があります。

まず第一に-普通の古いforloopsは普通で古いです。誰もがすでにそれらを知っています。

2番目に重要なのは、おそらくIterable.forEachを1行のラムダでのみ使用することです。 「ボディ」が重くなると、読みにくくなる傾向があります。ここには2つのオプションがあります-内部クラス(yuck)を使用するか、単純な古いforloopを使用します。同じコードベースで同じこと(コレクションよりもイテラチン)がさまざまなvay/styleで行われているのを見ると、人々はしばしばイライラします。これは事実のようです。

繰り返しますが、これは問題になる場合もあれば、そうでない場合もあります。コードの作業者に依存します。

5
Eugene Loy

最も厄介な機能的なforEachの制限の1つは、チェック例外のサポートの欠如です。

1つの 考えられる回避策 は、端末forEachを通常のforeachループに置き換えることです。

    Stream<String> stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty());
    Iterable<String> iterable = stream::iterator;
    for (String s : iterable) {
        fileWriter.append(s);
    }

以下は、ラムダおよびストリーム内のチェック済み例外処理に関する他の回避策を含む、最も一般的な質問のリストです。

例外をスローするJava 8 Lambda関数

Java 8:Lambda-Streams、例外付きメソッドによるフィルタリング

Java 8ストリーム内からCHECKED例外をスローするにはどうすればいいですか?

Java 8:ラムダ式での必須チェック例外処理なぜ必須であり、オプションではないのか

4
Vadzim

1.7拡張forループを超えるJava 1.8 forEachメソッドの利点は、コードを記述している間はビジネスロジックのみに集中できることです。

forEachメソッドはJava.util.function.Consumerオブジェクトを引数として取るので、 ビジネスロジックを別の場所に置くことができます。いつでも再利用できます。

下のスニペットを見てください、

  • ここでは、Consumer Classのacceptクラスメソッドをオーバーライドする新しいClassを作成しました。ここでは、追加の機能、More than Iteration .. !!!!!!を追加できます。

    class MyConsumer implements Consumer<Integer>{
    
        @Override
        public void accept(Integer o) {
            System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o);
        }
    }
    
    public class ForEachConsumer {
    
        public static void main(String[] args) {
    
            // Creating simple ArrayList.
            ArrayList<Integer> aList = new ArrayList<>();
            for(int i=1;i<=10;i++) aList.add(i);
    
            //Calling forEach with customized Iterator.
            MyConsumer consumer = new MyConsumer();
            aList.forEach(consumer);
    
    
            // Using Lambda Expression for Consumer. (Functional Interface) 
            Consumer<Integer> lambda = (Integer o) ->{
                System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o);
            };
            aList.forEach(lambda);
    
            // Using Anonymous Inner Class.
            aList.forEach(new Consumer<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
    
2
Hardik Patel