Het verschil tussen Collection.stream (). ForEach () en Collection.forEach ()

1. Inleiding

Er zijn verschillende opties om een ​​verzameling in Java te herhalen. In deze korte tutorial zullen we twee vergelijkbare benaderingen bekijken - Collection.stream (). ForEach () en Collection.forEach ().

In de meeste gevallen zullen beide dezelfde resultaten opleveren, maar er zijn enkele subtiele verschillen die we zullen bekijken.

2. Overzicht

Laten we eerst een lijst maken om te herhalen:

Lijst lijst = Arrays.asList ("A", "B", "C", "D");

De meest eenvoudige manier is om de verbeterde for-loop te gebruiken:

for (String s: list) {// doe iets met s} 

Als we Java in functionele stijl willen gebruiken, kunnen we ook de voor elke (). Dit kunnen we direct op de collectie doen:

Consument consument = s -> {System.out :: println}; list.forEach (consument); 

Of we kunnen bellen voor elke () op de stream van de collectie:

list.stream (). forEach (consument); 

Beide versies zullen de lijst herhalen en alle elementen afdrukken:

ABCD ABCD

In dit eenvoudige geval maakt het niet uit welke voor elke () we gebruiken.

3. Uitvoeringsbevel

Collection.forEach () gebruikt de iterator van de verzameling (als er een is opgegeven). Dat betekent dat de verwerkingsvolgorde van de artikelen wordt bepaald. Daarentegen is de verwerkingsvolgorde van Collection.stream (). ForEach () is niet gedefinieerd.

In de meeste gevallen maakt het niet uit welke van de twee we kiezen.

3.1. Parallelle stromen

Met parallelle streams kunnen we de stream in meerdere threads uitvoeren en in dergelijke situaties is de uitvoeringsvolgorde niet gedefinieerd. Java vereist alleen dat alle threads zijn voltooid voordat een terminalbewerking wordt uitgevoerd, zoals Collectors.toList (), wordt genoemd.

Laten we eens kijken naar een voorbeeld waar we eerst bellen voor elke () direct op de collectie, en ten tweede, op een parallelle stroom:

list.forEach (System.out :: print); System.out.print (""); list.parallelStream (). forEach (System.out :: print); 

Als we de code meerdere keren uitvoeren, zien we dat lijst.forEach () verwerkt de items in de invoegvolgorde, while lijst.parallelStream (). forEach () produceert bij elke run een ander resultaat.

Een mogelijke output is:

ABCD CDBA

Een andere is:

ABCD DBCA

3.2. Aangepaste iterators

Laten we een lijst definiëren met een aangepaste iterator om de verzameling in omgekeerde volgorde te herhalen:

klasse ReverseList breidt ArrayList uit {@Override public Iterator iterator () {int startIndex = this.size () - 1; Lijst lijst = dit; Iterator it = nieuwe Iterator () {privé int currentIndex = startIndex; @Override openbare boolean hasNext () {return currentIndex> = 0; } @Override public String next () {String next = list.get (currentIndex); currentIndex--; volgende terugkeer; } @Override public void remove () {gooi nieuwe UnsupportedOperationException (); }}; breng het terug; }} 

Wanneer we de lijst herhalen, opnieuw met voor elke () direct op de collectie en vervolgens op de stream:

Lijst myList = nieuwe ReverseList (); myList.addAll (lijst); myList.forEach (System.out :: print); System.out.print (""); myList.stream (). forEach (System.out :: print); 

We krijgen verschillende resultaten:

DCBA ABCD 

De reden voor de verschillende resultaten is dat voor elke () direct gebruikt in de lijst gebruikt de aangepaste iterator, terwijl stream (). forEach () neemt eenvoudig elementen een voor een uit de lijst, waarbij de iterator wordt genegeerd.

4. Wijziging van de collectie

Veel verzamelingen (bijv. ArrayList of HashSet) mag niet structureel worden gewijzigd terwijl u eroverheen gaat. Als een element wordt verwijderd of toegevoegd tijdens een iteratie, krijgen we een Gelijktijdige wijziging uitzondering.

Bovendien zijn verzamelingen ontworpen om te falen, wat betekent dat de uitzondering wordt gegenereerd zodra er een wijziging is.

Evenzo krijgen we een Gelijktijdige wijziging uitzondering wanneer we een element toevoegen of verwijderen tijdens de uitvoering van de stroompijplijn. De uitzondering wordt echter later gegenereerd.

Nog een subtiel verschil tussen de twee voor elke () methodes is dat Java expliciet het wijzigen van elementen toestaat met behulp van de iterator. Streams zouden daarentegen niet storend moeten zijn.

Laten we het verwijderen en wijzigen van elementen in meer detail bekijken.

4.1. Een element verwijderen

Laten we een bewerking definiëren die het laatste element ("D") van onze lijst verwijdert:

Consument removeElement = s -> {System.out.println (s + "" + list.size ()); if (s! = null && s.equals ("A")) {list.remove ("D"); }};

Wanneer we de lijst herhalen, wordt het laatste element verwijderd nadat het eerste element ("A") is afgedrukt:

list.forEach (removeElement);

Sinds voor elke () is fail-fast, we stoppen met itereren en zien een uitzondering voordat het volgende element wordt verwerkt:

Een 4 uitzondering in thread "main" java.util.ConcurrentModificationException op java.util.ArrayList.forEach (ArrayList.java:1252) op ReverseList.main (ReverseList.java:1)

Laten we eens kijken wat er gebeurt als we gebruiken stream (). forEach () in plaats daarvan:

list.stream (). forEach (removeElement);

Hier gaan we door met het herhalen van de hele lijst voordat we een uitzondering zien:

A 4 B 3 C 3 null 3 Uitzondering in thread "main" java.util.ConcurrentModificationException op java.util.ArrayList $ ArrayListSpliterator.forEachRemaining (ArrayList.java:1380) op java.util.stream.ReferencePipeline $ Head.forEach (ReferencePipeline .java: 580) op ReverseList.main (ReverseList.java:1)

Java garandeert echter niet dat een ConcurrentModificationException wordt überhaupt gegooid. Dat betekent dat we nooit een programma moeten schrijven dat afhankelijk is van deze uitzondering.

4.2. Elementen veranderen

We kunnen een element wijzigen terwijl we een lijst doorlopen:

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

Hoewel er geen probleem is om dit te doen, gebruikt u een van beide Collection.forEach () of stream (). forEach ()Vereist Java dat een bewerking op een stream niet storend is. Dit betekent dat elementen niet mogen worden gewijzigd tijdens de uitvoering van de streampijplijn.

De reden hierachter is dat de stream parallelle uitvoering moet vergemakkelijken. Hier kunnen het wijzigen van elementen van een stream tot onverwacht gedrag leiden.

5. Conclusie

In dit artikel hebben we enkele voorbeelden gezien die de subtiele verschillen tussen Collection.forEach () en Collection.stream (). ForEach ().

Het is echter belangrijk op te merken dat alle bovenstaande voorbeelden triviaal zijn en alleen bedoeld zijn om de twee manieren van itereren over een verzameling te vergelijken. We moeten geen code schrijven waarvan de juistheid afhangt van het getoonde gedrag.

Als we geen stream nodig hebben, maar alleen een verzameling willen herhalen, zou de eerste keuze moeten zijn om te gebruiken voor elke () direct op de collectie.

De broncode voor de voorbeelden in dit artikel is beschikbaar op GitHub.