1. Въведение
Има няколко опции за итерация над колекция в Java. В този кратък урок ще разгледаме два подобни търсещи подхода - Collection.stream (). ForEach () и Collection.forEach () .
В повечето случаи и двете ще дадат едни и същи резултати, но има някои фини разлики, които ще разгледаме.
2. Общ преглед
Първо, нека създадем списък, който да прегледаме:
List list = Arrays.asList("A", "B", "C", "D");
Най-ясният начин е използването на подобрения for-loop:
for(String s : list) { //do something with s }
Ако искаме да използваме функционален стил Java, можем да използваме и forEach () . Можем да направим това директно в колекцията:
Consumer 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 { @Override public Iterator iterator() { int startIndex = this.size() - 1; List list = this; Iterator it = new Iterator() { 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 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 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)
Let's see what happens if we use stream().forEach() instead:
list.stream().forEach(removeElement);
Here, we continue iterating over the whole list before we see an exception:
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)
However, Java does not guarantee that a ConcurrentModificationException is thrown at all. That means we should never write a program that depends on this exception.
4.2. Changing Elements
We can change an element while iterating over a list:
list.forEach(e -> { list.set(3, "E"); });
However, while there is no problem with doing this using either Collection.forEach() or stream().forEach(), Java requires an operation on a stream to be non-interfering. This means that elements shouldn't be modified during the execution of the stream pipeline.
The reason behind this is that the stream should facilitate parallel execution. Here, modifying elements of a stream could lead to unexpected behavior.
5. Conclusion
In this article, we saw some examples that show the subtle differences between Collection.forEach() and Collection.stream().forEach().
However, it's important to note that all the examples shown above are trivial and are merely meant to compare the two ways of iterating over a collection. We shouldn't write code whose correctness relies on the shown behavior.
If we don't require a stream but only want to iterate over a collection, the first choice should be using forEach() directly on the collection.
Изходният код за примерите в тази статия е достъпен в GitHub.