Collection.stream()。forEach()とCollection.forEach()の違い

1. 前書き

Javaのコレクションを反復処理するには、いくつかのオプションがあります。 *この短いチュートリアルでは、_Collection.stream()。forEach()_と_Collection.forEach()_ *という2つの類似したアプローチを見ていきます。
ほとんどの場合、両方で同じ結果が得られますが、いくつかの微妙な違いがあります。

2. 概要

最初に、反復するリストを作成しましょう。
List<String> list = Arrays.asList("A", "B", "C", "D");
最も簡単な方法は、拡張forループを使用することです。
for(String s : list) {
    //do something with s
}
関数型のJavaを使用する場合は、_forEach()_も使用できます。 コレクションで直接行うことができます。
Consumer<String> consumer = s -> { System.out::println };
list.forEach(consumer);
または、コレクションのストリームで_forEach()_を呼び出すことができます。
list.stream().forEach(consumer);
どちらのバージョンもリストを反復処理し、すべての要素を出力します:
ABCD ABCD
この単純なケースでは、どの_forEach()_を使用しても違いはありません。

3. 実行順序

  • _Collection.forEach()_は、コレクションの反復子(指定されている場合)を使用します。 つまり、アイテムの処理順序が定義されています。 対照的に、_Collection.stream()。forEach()_の処理順序は未定義です。*

    ほとんどの場合、どちらを選択しても違いはありません。

3.1. 並列ストリーム

並列ストリームを使用すると、複数のスレッドでストリームを実行できます。このような状況では、実行順序は未定義です。 Javaでは、_Collectors.toList()_などの端末操作が呼び出される前に、すべてのスレッドが終了する必要があります。
最初にコレクションで直接_forEach()_を呼び出し、次にパラレルストリームで呼び出した例を見てみましょう。
list.forEach(System.out::print);
System.out.print(" ");
list.parallelStream().forEach(System.out::print);
*コードを数回実行すると、_list.forEach()_は挿入順にアイテムを処理し、_list.parallelStream()。forEach()_は実行ごとに異なる結果を生成します。*
1つの可能な出力は次のとおりです。
ABCD CDBA
もう一つは:
ABCD DBCA

3.2. カスタムイテレーター

カスタムイテレータを使用してリストを定義し、コレクションを逆順に繰り返します。
class ReverseList extends ArrayList<String> {

    @Override
    public Iterator<String> iterator() {

        int startIndex = this.size() - 1;
        List<String> list = this;

        Iterator<String> it = new Iterator<String>() {

            private int currentIndex = startIndex;

            @Override
            public boolean hasNext() {
                return currentIndex >= 0;
            }

            @Override
            public String next() {
                String next = list.get(currentIndex);
                currentIndex--;
                return next;
             }

             @Override
             public void remove() {
                 throw new UnsupportedOperationException();
             }
         };
         return it;
    }
}
リストを繰り返し処理するとき、再びコレクションに対して直接_forEach()_を使用し、次にストリームに対して実行します。
List<String> myList = new ReverseList();
myList.addAll(list);

myList.forEach(System.out::print);
System.out.print(" ");
myList.stream().forEach(System.out::print);
異なる結果が得られます。
DCBA ABCD
結果が異なる理由は、リストで直接使用される_forEach()_がカスタムイテレータを使用するためです。

4. コレクションの変更

多くのコレクション(_ArrayList_や_HashSet_など)を繰り返し処理している間、構造を変更しないでください。 反復中に要素が削除または追加されると、_ConcurrentModification_例外が発生します。
さらに、コレクションはフェイルファーストになるように設計されています。つまり、変更があるとすぐに例外がスローされます。
*同様に、ストリームパイプラインの実行中に要素を追加または削除すると、_ConcurrentModification_例外が発生します。 ただし、例外は後でスローされます。*
2つの_forEach()_メソッドのもう1つの微妙な違いは、Javaがイテレーターを使用して要素を明示的に変更できることです。 対照的に、ストリームは干渉しないものでなければなりません。
要素の削除と変更について詳しく見てみましょう。

4.1. 要素を削除する

リストの最後の要素(「D」)を削除する操作を定義しましょう。
Consumer<String> removeElement = s -> {
    System.out.println(s + " " + list.size());
    if (s != null && s.equals("A")) {
        list.remove("D");
    }
};
リストを反復処理すると、最初の要素(「A」)が出力された後、最後の要素が削除されます。
list.forEach(removeElement);
  • _forEach()_はフェイルファーストであるため、次の要素が処理される前に反復を停止し、例外を確認します。*

A 4
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList.forEach(ArrayList.java:1252)
    at ReverseList.main(ReverseList.java:1)
代わりに_stream()。forEach()_を使用するとどうなるかを見てみましょう。
list.stream().forEach(removeElement);
*ここでは、例外が発生するまでリスト全体を繰り返し処理します:*
A 4
B 3
C 3
null 3
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380)
    at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
    at ReverseList.main(ReverseList.java:1)
ただし、Javaはhttps://docs.oracle.com/javase/8/docs/api/java/util/ConcurrentModificationException.html[_ConcurrentModificationException_]がスローされることをまったく保証しません。 *つまり、この例外に依存するプログラムを作成しないでください。*

4.2. 変化する要素

リストを反復しながら要素を変更できます。
list.forEach(e -> {
    list.set(3, "E");
});
ただし、_Collection.forEach()_または_stream()。forEach()_のいずれかを使用してこれを実行しても問題はありませんが、Javaはストリームに対する操作が非干渉である必要があります。 つまり、ストリームパイプラインの実行中に要素を変更しないでください。
この背後にある理由は、ストリームが並列実行を促進する必要があるためです。 *ここで、ストリームの要素を変更すると、予期しない動作が発生する可能性があります。*

5. 結論

この記事では、_Collection.forEach()_と_Collection.stream()。forEach()_の微妙な違いを示す例をいくつか見てきました。
ただし、上記の例はすべて些細なものであり、コレクションを反復処理する2つの方法を比較するためのものにすぎないことに注意することが重要です。 示された動作に正確性が依存するコードを記述しないでください。
*ストリームを必要とせず、コレクションのみを反復処理する場合、最初の選択肢はコレクションで直接_forEach()_を使用することです。*
この記事の例のソースコードhttps://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-streams[GitHubで入手可能]