Collection.stream() forEach()和Collection.forEach()之間的區別

1.簡介

有幾個選項可以遍歷Java中的集合。在這個簡短的教程中,我們將研究兩種相似的方法-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()的處理順序是不確定的。

在大多數情況下,我們選擇兩者中的哪一個沒有區別。

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()每次運行都會產生不同的結果。

一種可能的輸出是:

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()只是從列表中一個接一個地獲取元素,而忽略了迭代器。

4.修改收藏

許多集合(例如ArrayListHashSet )在對其進行迭代時不應在結構上進行修改。如果元素在迭代過程中被刪除或添加,我們將獲得ConcurrentModification異常。

此外,集合被設計為快速失敗的,這意味著在進行修改後立即引發異常。

同樣,在流管道的執行過程中添加或刪除元素時,將獲得ConcurrentModification異常。但是,異常將在以後引發。

這兩個forEach()方法之間的另一個細微差別是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()之間的細微差別。

但是,必須注意的是,上面顯示的所有示例都是微不足道的,僅用於比較對集合進行迭代的兩種方式。我們不應該編寫其正確性取決於所顯示行為的代碼。

如果我們不需要流,而只想遍歷集合,則首選應該直接在集合上使用forEach()

本文示例的源代碼可從GitHub上獲得。