Gids voor Stream.reduce ()

1. Overzicht

De Stream API biedt een rijk repertoire van tussenliggende, reductie- en terminalfuncties, die ook parallellisatie ondersteunen.

Specifieker, Reductiestroombewerkingen stellen ons in staat om één enkel resultaat te produceren uit een reeks elementen, door herhaaldelijk een combinatiebewerking toe te passen op de elementen in de reeks.

In deze tutorial we zullen kijken naar het algemene doel Stream.reduce () operatie en zie het in enkele concrete use-cases.

2. De kernbegrippen: identiteit, accumulator en combinatie

Voordat we dieper ingaan op het gebruik van de Stream.reduce () operatie, laten we de deelnemerselementen van de operatie opsplitsen in afzonderlijke blokken. Op die manier zullen we gemakkelijker de rol begrijpen die elk speelt:

  • Identiteit - een element dat de beginwaarde is van de reductiebewerking en het standaardresultaat als de stream leeg is
  • Accumulator - een functie die twee parameters nodig heeft: een gedeeltelijk resultaat van de reductieoperatie en het volgende element van de stream
  • Combiner - een functie die wordt gebruikt om het gedeeltelijke resultaat van de reductiebewerking te combineren wanneer de reductie parallel loopt, of wanneer er een discrepantie is tussen de typen accumulatorargumenten en de typen van de accumulatorimplementatie

3. Met behulp van Stream.reduce ()

Laten we enkele basisvoorbeelden bekijken om de functionaliteit van de identiteits-, accumulator- en combiner-elementen beter te begrijpen:

Lijstnummers = Arrays.asList (1, 2, 3, 4, 5, 6); int resultaat = getallen .stream () .reduce (0, (subtotaal, element) -> subtotaal + element); assertThat (resultaat) .isEqualTo (21);

In dit geval, de Geheel getal waarde 0 is de identiteit. Het slaat de initiële waarde van de reductiebewerking op, en ook het standaardresultaat wanneer de stream van Geheel getal waarden is leeg.

Hetzelfde, de lambda-uitdrukking:

subtotaal, element -> subtotaal + element

is de accumulator, aangezien het de gedeeltelijke som van Geheel getal waarden en het volgende element in de stream.

Om de code nog beknopter te maken, kunnen we een methodeverwijzing gebruiken in plaats van een lambda-uitdrukking:

int resultaat = nummers.stream (). reduceren (0, geheel getal :: som); assertThat (resultaat) .isEqualTo (21);

Natuurlijk kunnen we een verminderen() werking op stromen die andere soorten elementen bevatten.

We kunnen bijvoorbeeld gebruiken verminderen() op een reeks van Draad elementen en voeg ze samen tot één resultaat:

Lijstletters = Arrays.asList ("a", "b", "c", "d", "e"); String resultaat = letters .stream () .reduce ("", (gedeeltelijkeString, element) -> partiëleString + element); assertThat (resultaat) .isEqualTo ("abcde");

Evenzo kunnen we overschakelen naar de versie die een methodeverwijzing gebruikt:

String resultaat = letters.stream (). Reduc ("", String :: concat); assertThat (resultaat) .isEqualTo ("abcde");

Laten we de verminderen() bewerking voor het samenvoegen van de hoofdletters van de brieven matrix:

String resultaat = letters .stream () .reduce ("", (gedeeltelijkeString, element) -> partiëleString.toUpperCase () + element.toUpperCase ()); assertThat (resultaat) .isEqualTo ("ABCDE");

Daarnaast kunnen we gebruik maken van verminderen() in een parallelle stroom (hierover later meer):

Lijst leeftijden = Arrays.asList (25, 30, 45, 28, 32); int computedAges = leeftijden.parallelStream (). verminderen (0, a, b -> a + b, geheel getal :: som);

Wanneer een stream parallel wordt uitgevoerd, splitst de Java-runtime de stream op in meerdere substreams. In dergelijke gevallen, we moeten een functie gebruiken om de resultaten van de deelstromen in één enkele te combineren. Dit is de rol van de combiner - in het bovenstaande fragment is het de Geheel getal :: som methode referentie.

Grappig genoeg zal deze code niet compileren:

Lijst gebruikers = Arrays.asList (nieuwe gebruiker ("John", 30), nieuwe gebruiker ("Julie", 35)); int computedAges = gebruikers.stream (). reduceren (0, (PartialAgeResult, user) -> PartialAgeResult + user.getAge ()); 

In dit geval hebben we een stroom van Gebruiker objecten, en de typen van de accumulatorargumenten zijn Geheel getal en Gebruiker. De implementatie van de accumulator is echter een som van Gehele getallen, dus de compiler kan het type gebruiker parameter.

We kunnen dit probleem oplossen door een combiner te gebruiken:

int resultaat = gebruikers.stream () .reduce (0, (partiëleAgeResult, gebruiker) -> partiëleAgeResult + gebruiker.getAge (), geheel getal :: som); assertThat (resultaat) .isEqualTo (65);

Simpel gezegd, als we opeenvolgende streams gebruiken en de typen van de accumulatorargumenten en de typen van de implementatie-overeenkomst, hoeven we geen combiner te gebruiken.

4. Parallel verminderen

Zoals we eerder hebben geleerd, kunnen we gebruiken verminderen() op parallelle stromen.

Als we parallelle streams gebruiken, moeten we ervoor zorgen dat verminderen() of andere geaggregeerde bewerkingen die op de streams worden uitgevoerd, zijn:

  • associatief: het resultaat wordt niet beïnvloed door de volgorde van de operanden
  • niet-interfererend: de bewerking heeft geen invloed op de gegevensbron
  • staatloos en deterministisch: de bewerking heeft geen status en produceert dezelfde uitvoer voor een bepaalde invoer

We moeten aan al deze voorwaarden voldoen om onvoorspelbare resultaten te voorkomen.

Zoals verwacht werden bewerkingen uitgevoerd op parallelle streams, inclusief verminderen(), worden parallel uitgevoerd, waardoor ze profiteren van multi-core hardware-architecturen.

Voor duidelijke redenen, parallelle streams zijn veel performanter dan de opeenvolgende tegenhangers. Toch kunnen ze overdreven zijn als de bewerkingen die op de stream worden toegepast niet duur zijn, of als het aantal elementen in de stream klein is.

Natuurlijk zijn parallelle streams de juiste manier om te gaan als we met grote streams moeten werken en dure geaggregeerde bewerkingen moeten uitvoeren.

Laten we een eenvoudige JMH-benchmarktest (de Java Microbenchmark Harness) maken en de respectieve uitvoeringstijden vergelijken bij het gebruik van de verminderen() werking op een sequentiële en een parallelle stroom:

@State (Scope.Thread) privé definitieve lijst userList = createUsers (); @Benchmark openbaar geheel getal executeReduceOnParallelizedStream () {retourneer this.userList .parallelStream () .reduce (0, (gedeeltelijkeAgeResult, gebruiker) -> partiëleAgeResult + gebruiker.getAge (), geheel getal :: som); } @Benchmark openbaar geheel getal executeReduceOnSequentialStream () {retourneer this.userList .stream () .reduce (0, (PartialAgeResult, gebruiker) -> PartialAgeResult + user.getAge (), Geheel getal :: som); } 

In de bovenstaande JMH-benchmark vergelijken we de gemiddelde uitvoeringstijden. We maken gewoon een Lijst met een groot aantal Gebruiker voorwerpen. Vervolgens bellen we verminderen() op een sequentiële en een parallelle stream en controleer of de laatste sneller presteert dan de eerste (in seconden per bewerking).

Dit zijn onze benchmarkresultaten:

Benchmarkmodus Cnt Score Fout Eenheden JMHStreamReduceBenchMark.executeReduceOnParallelizedStream gemiddelde 5 0,007 ± 0,001 s / op JMHStreamReduceBenchMark.executeReduceOnSequentialStream gemiddelde 5 0,010 ± 0,001 s / op

5. Uitzonderingen gooien en behandelen tijdens het verminderen

In de bovenstaande voorbeelden is de verminderen() operatie genereert geen uitzonderingen. Maar het kan natuurlijk.

Stel bijvoorbeeld dat we alle elementen van een stroom moeten delen door een opgegeven factor en ze vervolgens moeten optellen:

Lijstnummers = Arrays.asList (1, 2, 3, 4, 5, 6); int deler = 2; int resultaat = nummers.stream (). verminderen (0, a / deler + b / deler); 

Dit zal werken, zolang het scheidingslijn variabele is niet nul. Maar als het nul is, verminderen() zal een ArithmeticException uitzondering: deel door nul.

We kunnen de uitzondering gemakkelijk opvangen en er iets nuttigs mee doen, zoals het loggen, ervan herstellen, enzovoort, afhankelijk van het gebruik, door een try / catch-blok te gebruiken:

public static int divideListElements (List values, int divider) {return values.stream () .reduce (0, (a, b) -> {try {return a / divider + b / divider;} catch (ArithmeticException e) {LOGGER .log (Level.INFO, "Rekenkundige uitzondering: delen door nul");} return 0;}); }

Hoewel deze aanpak zal werken, we vervuilden de lambda-uitdrukking met de proberen te vangen blok. We hebben niet langer de schone oneliner die we eerder hadden.

Om dit probleem op te lossen, kunnen we de refactoring-techniek van de extractiefunctie gebruikenen pak het proberen te vangen blok in een aparte methode:

privé statische int verdelen (int waarde, int factor) {int resultaat = 0; probeer {resultaat = waarde / factor; } catch (ArithmeticException e) {LOGGER.log (Level.INFO, "Rekenkundige uitzondering: delen door nul"); } resultaat retourneren} 

Nu is de implementatie van de divideListElements () methode is weer schoon en gestroomlijnd:

openbare statische int divideListElements (Lijstwaarden, int divider) {return values.stream (). reduc (0, (a, b) -> divide (a, divider) + divide (b, divider)); } 

In de veronderstelling dat divideListElements () is een gebruiksmethode geïmplementeerd door een abstract NumberUtils class, kunnen we een unit-test maken om het gedrag van de divideListElements () methode:

Lijstnummers = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (getallen, 1)). isEqualTo (21); 

Laten we ook het divideListElements () methode, wanneer de meegeleverde Lijst van Geheel getal waarden bevat een 0:

Lijstnummers = Arrays.asList (0, 1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (getallen, 1)). isEqualTo (21); 

Laten we tot slot de implementatie van de methode testen als de verdeler ook 0 is:

Lijstnummers = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (getallen, 0)). isEqualTo (0);

6. Complexe aangepaste objecten

Wij kan ook gebruiken Stream.reduce () met aangepaste objecten die niet-primitieve velden bevatten. Om dit te doen, moeten we een relevante identity, accumulator, en combiner voor het gegevenstype.

Stel dat onze Gebruiker maakt deel uit van een beoordelingswebsite. Elk van onze Gebruikers kunnen er een bezitten Beoordeling, die over veel wordt gemiddeld Recensies.

Laten we eerst beginnen met onze Recensie voorwerp. Elk Recensie moet een eenvoudige opmerking en score bevatten:

public class Review {privé int punten; privé String review; // constructeur, getters en setters}

Vervolgens moeten we onze Beoordeling, die onze beoordelingen naast een punten veld. Naarmate we meer recensies toevoegen, wordt dit veld dienovereenkomstig vergroot of verkleind:

public class Rating {dubbele punten; Lijstrecensies = nieuwe ArrayList (); public void add (Review review) {reviews.add (review); computeRating (); } private double computeRating () {double totalPoints = reviews.stream (). map (Review :: getPoints) .reduce (0, geheel getal :: som); this.points = totalPoints / reviews.size (); retourneer this.points; } publiek statisch gemiddelde beoordeling (beoordeling r1, beoordeling r2) {beoordeling gecombineerd = nieuwe beoordeling (); combined.reviews = nieuwe ArrayList (r1.reviews); combined.reviews.addAll (r2.reviews); combined.computeRating (); terugkeer gecombineerd; }}

We hebben ook een gemiddelde functie om een ​​gemiddelde te berekenen op basis van de twee invoer Beoordelings. Dit zal goed werken voor ons combiner en accumulator componenten.

Laten we vervolgens een lijst definiëren met Gebruikers, elk met hun eigen beoordelingen.

Gebruiker john = nieuwe gebruiker ("Jan", 30); john.getRating (). add (nieuwe recensie (5, "")); john.getRating (). add (nieuwe recensie (3, "niet slecht")); Gebruiker julie = nieuwe gebruiker ("Julie", 35); john.getRating (). add (nieuwe recensie (4, "geweldig!")); john.getRating (). add (nieuwe recensie (2, "vreselijke ervaring")); john.getRating (). add (nieuwe recensie (4, "")); Lijst gebruikers = Arrays.asList (john, julie); 

Nu er rekening is gehouden met John en Julie, gaan we gebruiken Stream.reduce () om een ​​gemiddelde beoordeling voor beide gebruikers te berekenen. Als een identiteit, laten we een nieuwe retourneren Beoordeling als onze invoerlijst leeg is:

Rating averageRating = users.stream () .reduce (nieuwe Rating (), (rating, user) -> Rating.average (rating, user.getRating ()), Rating :: gemiddeld);

Als we de wiskunde doen, zouden we moeten vaststellen dat de gemiddelde score 3,6 is:

assertThat (averageRating.getPoints ()). isEqualTo (3.6);

7. Conclusie

In deze tutorial we hebben geleerd hoe we de Stream.reduce () operatie. Daarnaast hebben we geleerd hoe we reducties kunnen uitvoeren op sequentiële en parallel geschakelde streams, en hoe we uitzonderingen kunnen afhandelen tijdens het reduceren.

Zoals gewoonlijk zijn alle codevoorbeelden die in deze tutorial worden getoond, beschikbaar op GitHub.


$config[zx-auto] not found$config[zx-overlay] not found