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.修改收藏
許多集合(例如ArrayList或HashSet )在對其進行迭代時不應在結構上進行修改。如果元素在迭代過程中被刪除或添加,我們將獲得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上獲得。