1. 概要

Javaでコレクションを反復処理するためのいくつかのオプションがあります。 この短いチュートリアルでは、2つの似たようなアプローチを見ていきます— Collection.stream()。forEach()とCollection.forEach()。

ほとんどの場合、どちらも同じ結果になりますが、いくつかの微妙な違いを見ていきます。

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()の処理順序は未定義です。

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

3.1. パラレルストリーム

並列ストリームを使用すると、複数のスレッドでストリームを実行できます。このような状況では、実行順序は定義されていません。 Javaでは、 Collectors.toList()などの端末操作が呼び出される前に、すべてのスレッドを終了する必要があります。

最初にコレクションでforEach()を直接呼び出し、次に並列ストリームで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()はカスタムイテレータを使用するのに対し、stream()。forEach()はリストから要素を1つずつ取得し、イテレータ。

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は、ConcurrentModificationExceptionがスローされることをまったく保証しません。 つまり、この例外に依存するプログラムを作成してはいけません。

4.2. 要素の変更

リストを反復処理しながら要素を変更できます。

list.forEach(e -> {
    list.set(3, "E");
});

ただし、 Collection.forEach()または stream()。forEach()を使用してこれを行うことに問題はありませんが、Javaではストリームに対する操作が干渉しないようにする必要があります。 これは、ストリームパイプラインの実行中に要素を変更してはならないことを意味します。

この背後にある理由は、ストリームが並列実行を容易にする必要があるためです。 ここで、ストリームの要素を変更すると、予期しない動作が発生する可能性があります。

5. 結論

この記事では、 Collection.forEach() Collection.stream()。forEach()の微妙な違いを示すいくつかの例を見ました。

上記のすべての例は些細なものであり、コレクションを反復処理する2つの方法のみを比較することを目的としていることに注意することが重要です。 示されている動作に正確さが依存するコードを記述しないでください。

ストリームを必要とせず、コレクションを反復処理するだけの場合、最初の選択肢は、コレクションで直接 forEach()を使用することです。

この記事の例のソースコードは、GitHubから入手できます。