Guide til Stream.reduce ()

1. Oversigt

Stream API giver et rigt repertoire af mellem-, reduktions- og terminalfunktioner, som også understøtter parallelisering.

Mere specifikt, reduktionsstrømoperationer giver os mulighed for at producere et enkelt resultat ud fra en række af elementerved gentagne gange at anvende en kombinationsoperation på elementerne i sekvensen.

I denne vejledning vi ser på det generelle formål Stream.reduce () operation og se det i nogle konkrete brugssager.

2. Nøglebegreberne: Identitet, Akkumulator og Combiner

Før vi ser dybere på brugen af Stream.reduce () operation, lad os opdele operationens deltagende elementer i separate blokke. På den måde forstår vi lettere den rolle, som hver spiller:

  • Identitet - et element, der er den indledende værdi af reduktionsoperationen og standardresultatet, hvis strømmen er tom
  • Akkumulator - en funktion, der tager to parametre: et delvis resultat af reduktionsoperationen og det næste element i strømmen
  • Combiner - en funktion, der bruges til at kombinere det delvise resultat af reduktionsoperationen, når reduktionen er paralleliseret, eller når der er et misforhold mellem typerne af akkumulatorargumenterne og typerne af akkumulatorimplementeringen

3. Brug Stream.reduce ()

For bedre at forstå funktionaliteten af ​​identitets-, akkumulator- og kombinationselementerne, lad os se på nogle grundlæggende eksempler:

Listenumre = Arrays.asList (1, 2, 3, 4, 5, 6); int-resultat = tal .stream () .reduce (0, (subtotal, element) -> subtotal + element); assertThat (resultat) .isEqualTo (21);

I dette tilfælde, det Heltal værdi 0 er identiteten. Den gemmer den indledende værdi af reduktionsoperationen og også standardresultatet, når strømmen af Heltal værdier er tomme.

Ligeledes, lambda-udtrykket:

subtotal, element -> subtotal + element

er akkumulatoren, da det tager den delvise sum af Heltal værdier og det næste element i strømmen.

For at gøre koden endnu mere kortfattet kan vi bruge en metodehenvisning i stedet for et lambda-udtryk:

int resultat = numbers.stream (). reducer (0, Integer :: sum); hævder, at (resultat) .isEqualTo (21);

Selvfølgelig kan vi bruge en reducere() drift på vandløb, der indeholder andre typer elementer.

For eksempel kan vi bruge reducere() på en række Snor elementer og slut dem til et enkelt resultat:

Listebogstaver = Arrays.asList ("a", "b", "c", "d", "e"); Strengresultat = bogstaver .stream () .reduce ("", (partialString, element) -> partialString + element); assertThat (resultat) .isEqualTo ("abcde");

På samme måde kan vi skifte til den version, der bruger en metodereference:

String result = letters.stream (). Reduce ("", String :: concat); assertThat (resultat) .isEqualTo ("abcde");

Lad os bruge reducere() operation til sammenføjning af de store bogstaver i bogstaver matrix:

Strengresultat = bogstaver .stream () .reduce ("", (partialString, element) -> partialString.toUpperCase () + element.toUpperCase ()); assertThat (resultat) .isEqualTo ("ABCDE");

Derudover kan vi bruge reducere() i en paralleliseret strøm (mere om dette senere):

Liste aldre = Arrays.asList (25, 30, 45, 28, 32); int computedAges = ages.parallelStream (). reducer (0, a, b -> a + b, Heltal :: sum);

Når en stream udføres parallelt, opdeler Java runtime strømmen i flere understrømme. I sådanne tilfælde, vi er nødt til at bruge en funktion til at kombinere resultaterne af understrømmene til en enkelt. Dette er kombinatorens rolle - i ovenstående uddrag er det Heltal :: sum metodehenvisning.

Sjovt nok kompilerer denne kode ikke:

Listebrugere = Arrays.asList (ny bruger ("John", 30), ny bruger ("Julie", 35)); int computedAges = users.stream (). reducere (0, (partialAgeResult, bruger) -> partialAgeResult + user.getAge ()); 

I dette tilfælde har vi en strøm af Bruger objekter, og typen af ​​akkumulatorargumenter er Heltal og Bruger. Imidlertid er akkumulatorimplementeringen en sum af Heltal, så kompilatoren kan bare ikke udlede typen af bruger parameter.

Vi kan løse dette problem ved hjælp af en combiner:

int resultat = brugere.strøm () .reduce (0, (partialAgeResult, bruger) -> partialAgeResult + user.getAge (), Heltal :: sum); hævder, at (resultat) .isEqualTo (65);

For at sige det enkelt, hvis vi bruger sekventielle streams og typerne af akkumulatorargumenterne og typerne af dens implementering matcher, behøver vi ikke bruge en combiner.

4. Reduktion parallelt

Som vi lærte før, kan vi bruge reducere() på paralleliserede vandløb.

Når vi bruger parallelle strømme, skal vi sørge for det reducere() eller enhver anden samlet operation udført på streams er:

  • associerende: resultatet påvirkes ikke af rækkefølgen af ​​operanderne
  • ikke-forstyrrende: handlingen påvirker ikke datakilden
  • statsløs og deterministisk: operationen har ikke tilstand og producerer den samme output for en given input

Vi bør opfylde alle disse betingelser for at forhindre uforudsigelige resultater.

Som forventet udføres operationer på paralleliserede strømme, herunder reducere(), udføres parallelt og drager derfor fordel af multi-core hardwarearkitekturer.

Af åbenlyse grunde parallelle strømme er meget mere performante end de sekventielle modstykker. Alligevel kan de være for store, hvis de operationer, der anvendes til strømmen, ikke er dyre, eller antallet af elementer i strømmen er lille.

Naturligvis er paralleliserede streams den rigtige vej at gå, når vi har brug for at arbejde med store streams og udføre dyre samlede operationer.

Lad os oprette en simpel JMH (Java Microbenchmark Harness) benchmark test og sammenligne de respektive udførelsestider, når du bruger reducere() operation på en sekventiel og en paralleliseret strøm:

@State (Scope.Thread) privat endelig Liste userList = createUsers (); @Benchmark public Integer executeReduceOnParallelizedStream () {return this.userList .parallelStream () .reduce (0, (partialAgeResult, user) -> partialAgeResult + user.getAge (), Integer :: sum); } @Benchmark public Integer executeReduceOnSequentialStream () {return this.userList .stream () .reduce (0, (partialAgeResult, user) -> partialAgeResult + user.getAge (), Integer :: sum); } 

I ovenstående JMH-benchmark sammenligner vi gennemsnitstider for udførelse. Vi opretter simpelthen en Liste indeholdende et stort antal Bruger genstande. Dernæst kalder vi reducere() på en sekventiel og en paralleliseret strøm og kontroller, at sidstnævnte fungerer hurtigere end den førstnævnte (i sekunder pr. operation).

Dette er vores benchmark-resultater:

Benchmark-tilstand Cnt Score Fejlenheder JMHStreamReduceBenchMark.executeReduceOnParallelizedStream avgt 5 0,007 ± 0,001 s / op JMHStreamReduceBenchMark.executeReduceOnSequentialStream avgt 5 0,010 ± 0,001 s / op

5. At kaste og håndtere undtagelser under reduktion

I ovenstående eksempler er reducere() operation kaster ingen undtagelser. Men det kan det selvfølgelig.

Sig for eksempel, at vi skal dele alle elementerne i en strøm med en leveret faktor og derefter summere dem:

Listenumre = Arrays.asList (1, 2, 3, 4, 5, 6); int divider = 2; int resultat = numbers.stream (). reducer (0, a / divider + b / divider); 

Dette fungerer, så længe skillevæg variabel er ikke nul. Men hvis det er nul, reducere() vil kaste en Aritmetisk undtagelse undtagelse: divider med nul.

Vi kan let fange undtagelsen og gøre noget nyttigt med den, såsom at logge den, gendanne sig efter den og så videre, afhængigt af brugssagen, ved hjælp af en prøve / fangst-blok:

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, "Aritmetisk undtagelse: Division efter nul");} return 0;}); }

Mens denne tilgang vil fungere, vi forurenede lambda-udtrykket med prøv / fange blok. Vi har ikke længere den rene enforing, vi havde før.

For at løse dette problem kan vi bruge teknikken til refactoring af ekstraktfunktionenog udpakke prøv / fange blokere i en separat metode:

privat statisk int divide (int værdi, int faktor) {int resultat = 0; prøv {resultat = værdi / faktor; } fange (ArithmeticException e) {LOGGER.log (Level.INFO, "Arithmetic Exception: Division by Zero"); } returner resultat} 

Nu er implementeringen af divideListElements () metoden er igen ren og strømlinet:

public static int divideListElements (List values, int divider) {return values.stream (). reduce (0, (a, b) -> divide (a, divider) + divide (b, divider)); } 

Antages det divideListElements () er en hjælpemetode implementeret af et abstrakt NumberUtils klasse, kan vi oprette en enhedstest for at kontrollere adfærden for divideListElements () metode:

Listenumre = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (tal, 1)). er EqualTo (21); 

Lad os også teste divideListElements () metode, når den leverede Liste af Heltal værdier indeholder et 0:

Listenumre = Arrays.asList (0, 1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (tal, 1)). er EqualTo (21); 

Lad os endelig teste metodeimplementeringen, når skillelinjen også er 0:

Listenumre = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (tal, 0)). erEqualTo (0);

6. Komplekse brugerdefinerede objekter

Vi kan også bruge Stream.reduce () med brugerdefinerede objekter, der indeholder ikke-primitive felter. For at gøre det skal vi give et relevant itandpleje, akkumulator, og kombinator for datatypen.

Antag vores Bruger er en del af en gennemgangswebsted. Hver af vores Brugers kan have en Bedømmelse, som er gennemsnit over mange Anmeldelses.

Lad os først starte med vores Anmeldelse objekt. Hver Anmeldelse skal indeholde en simpel kommentar og score:

offentlig klasse anmeldelse {private int point; privat streng anmeldelse; // konstruktør, getters og setters}

Dernæst skal vi definere vores Bedømmelse, som holder vores anmeldelser sammen med en point Mark. Når vi tilføjer flere anmeldelser, øges eller formindskes dette felt tilsvarende:

Offentlig klassificering {dobbeltpoint; Listeanmeldelser = ny ArrayList (); public void add (Review review) {reviews.add (review); computeRating (); } privat dobbelt computeRating () {dobbelt totalpoint = anmeldelser.strøm (). kort (anmeldelse :: getPoints) .reduktion (0, heltal :: sum); this.points = totalPoints / reviews.size (); returner dette. point; } offentligt statisk Rating gennemsnit (Rating r1, Rating r2) {Rating kombineret = ny Rating (); combined.reviews = new ArrayList (r1.reviews); combined.reviews.addAll (r2.reviews); combined.computeRating (); afkast kombineret }}

Vi har også tilføjet en gennemsnit funktion til at beregne et gennemsnit baseret på de to input Bedømmelses. Dette vil fungere godt for vores kombinator og akkumulator komponenter.

Lad os derefter definere en liste over Brugers, hver med deres egne sæt anmeldelser.

Bruger john = ny bruger ("John", 30); john.getRating (). tilføj (ny anmeldelse (5, "")); john.getRating (). tilføj (ny anmeldelse (3, "ikke dårligt")); Bruger julie = ny bruger ("Julie", 35); john.getRating (). tilføj (ny anmeldelse (4, "fantastisk!")); john.getRating (). tilføj (ny anmeldelse (2, "frygtelig oplevelse")); john.getRating (). tilføj (ny anmeldelse (4, "")); Listebrugere = Arrays.asList (john, julie); 

Nu hvor John og Julie redegøres for, lad os bruge det Stream.reduce () at beregne en gennemsnitlig vurdering på tværs af begge brugere. Som en identitet, lad os returnere et nyt Bedømmelse hvis vores inputliste er tom:

Bedømmelse gennemsnitRating = brugere.strøm () .reduktion (ny vurdering (), (vurdering, bruger) -> Bedømmelse. gennemsnit (vurdering, bruger.getRating ()), Bedømmelse :: gennemsnit);

Hvis vi laver matematik, skal vi finde ud af, at den gennemsnitlige score er 3,6:

assertThat (averageRating.getPoints ()). er EqualTo (3.6);

7. Konklusion

I denne vejledning vi lærte at bruge Stream.reduce () operation. Derudover lærte vi, hvordan man udfører reduktioner på sekventielle og paralleliserede streams, og hvordan man håndterer undtagelser, mens man reducerer.

Som normalt er alle kodeeksempler vist i denne vejledning tilgængelige på GitHub.


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