Guide til Stream.reduce ()

1. Oversikt

Stream API gir et rikt repertoar av mellom-, reduksjons- og terminalfunksjoner, som også støtter parallellisering.

Mer spesifikt, reduksjonsstrømoperasjoner tillater oss å produsere ett enkelt resultat fra en sekvens av elementer, ved gjentatte ganger å bruke en kombinasjonsoperasjon på elementene i sekvensen.

I denne veiledningen, vi vil se på den generelle hensikten Stream.reduce () operasjon og se det i noen konkrete brukssaker.

2. Nøkkelbegrepene: Identitet, Akkumulator og Kombinator

Før vi ser dypere på å bruke Stream.reduce () operasjon, la oss dele opp operasjonens deltakerelementer i separate blokker. På den måten forstår vi lettere hvilken rolle hver spiller:

  • Identitet - et element som er den opprinnelige verdien av reduksjonsoperasjonen og standardresultatet hvis strømmen er tom
  • Akkumulator - en funksjon som tar to parametere: et delvis resultat av reduksjonsoperasjonen og det neste elementet i strømmen
  • Combiner - en funksjon som brukes til å kombinere det delvise resultatet av reduksjonsoperasjonen når reduksjonen er parallellisert, eller når det er et misforhold mellom typene akkumulatorargumenter og typene akkumulatorimplementering

3. Bruke Stream.reduce ()

For å bedre forstå funksjonaliteten til identitets-, akkumulator- og kombinasjonselementene, la oss se på noen grunnleggende eksempler:

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

I dette tilfellet, de Heltall verdi 0 er identiteten. Den lagrer den opprinnelige verdien av reduksjonsoperasjonen, og også standardresultatet når strømmen av Heltall verdiene er tomme.

Like måte, lambda-uttrykket:

subtotal, element -> subtotal + element

er akkumulatoren, siden det tar delsummen av Heltall verdier og neste element i strømmen.

For å gjøre koden enda mer kortfattet, kan vi bruke en metodehenvisning, i stedet for et lambdauttrykk:

int resultat = tall.strøm (). reduser (0, Heltall :: sum); assertThat (resultat) .isEqualTo (21);

Selvfølgelig kan vi bruke en redusere() drift på bekker som holder andre typer elementer.

For eksempel kan vi bruke redusere() på en rekke String elementer og koble dem til et enkelt resultat:

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

På samme måte kan vi bytte til versjonen som bruker en metodereferanse:

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

La oss bruke redusere() operasjon for å bli med de store bokstavene i bokstaver matrise:

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

I tillegg kan vi bruke redusere() i en parallellisert strøm (mer om dette senere):

Listealder = Arrays.asList (25, 30, 45, 28, 32); int computedAges = ages.parallelStream (). reduser (0, a, b -> a + b, Heltall :: sum);

Når en strøm kjøres parallelt, deler Java-kjøretiden strømmen i flere delstrømmer. I slike tilfeller, vi må bruke en funksjon for å kombinere resultatene av delstrømmene til en enkelt. Dette er rollen som kombinereren - i utdraget ovenfor er det Heltall :: sum metodehenvisning.

Morsomt nok, denne koden vil ikke kompilere:

Liste brukere = Arrays.asList (ny bruker ("John", 30), ny bruker ("Julie", 35)); int computedAges = users.stream (). redusere (0, (partialAgeResult, bruker) -> partialAgeResult + user.getAge ()); 

I dette tilfellet har vi en strøm av Bruker objekter, og hvilke typer akkumulatorargumenter som er Heltall og Bruker. Imidlertid er akkumulatorimplementeringen en sum av Heltall, så kompilatoren kan bare ikke utlede typen av bruker parameter.

Vi kan løse dette problemet ved å bruke en kombinator:

int result = users.stream () .reduce (0, (partialAgeResult, user) -> partialAgeResult + user.getAge (), Integer :: sum); assertThat (resultat) .isEqualTo (65);

For å si det enkelt, hvis vi bruker sekvensielle strømmer og typene akkumulatorargumenter og typene av implementeringen samsvarer, trenger vi ikke bruke en kombinator.

4. Reduksjon i parallell

Som vi lærte før, kan vi bruke redusere() på parallelliserte bekker.

Når vi bruker parallelliserte strømmer, bør vi sørge for det redusere() eller andre aggregerte operasjoner utført på bekkene er:

  • assosiativ: resultatet påvirkes ikke av operandenes rekkefølge
  • ikke-forstyrrende: operasjonen påvirker ikke datakilden
  • statsløs og deterministisk: operasjonen har ikke tilstand og gir samme utgang for en gitt inngang

Vi bør oppfylle alle disse vilkårene for å forhindre uforutsigbare resultater.

Som forventet ble operasjoner utført på parallelliserte strømmer, inkludert redusere(), blir utført parallelt, og dermed utnytte multi-core maskinvarearkitekturer.

Av åpenbare grunner, parallelliserte strømmer er mye mer performante enn de sekvensielle kolleger. Allikevel kan de være overkill hvis operasjonene som brukes til strømmen ikke er dyre, eller hvis antallet elementer i strømmen er lite.

Parallelliserte strømmer er selvfølgelig den rette veien å gå når vi trenger å jobbe med store strømmer og utføre dyre samlede operasjoner.

La oss lage en enkel JMH (Java Microbenchmark Harness) referansetest og sammenligne de respektive utføringstidene når vi bruker redusere() operasjon på en sekvensiell og en parallellisert strøm:

@State (Scope.Thread) private final Liste brukerListe = 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 den ovennevnte JMH-referanseverdien sammenligner vi gjennomføringstidene. Vi oppretter ganske enkelt en Liste inneholder et stort antall Bruker gjenstander. Deretter ringer vi redusere() på en sekvensiell og en parallellisert strøm og sjekk at sistnevnte presterer raskere enn den tidligere (i sekunder per operasjon).

Dette er våre referanseresultater:

Referansemodus Cnt Score Error Units JMHStreamReduceBenchMark.executeReduceOnParallelizedStream avgt 5 0,007 ± 0,001 s / op JMHStreamReduceBenchMark.executeReduceOnSequentialStream avgt 5 0,010 ± 0,001 s / op

5. Kaste og håndtere unntak mens du reduserer

I eksemplene ovenfor er redusere() operasjonen gir ingen unntak. Men det kan, selvfølgelig.

Si for eksempel at vi trenger å dele alle elementene i en strøm med en tilført faktor og deretter summere dem:

Listetall = Arrays.asList (1, 2, 3, 4, 5, 6); int divider = 2; int resultat = tall.strøm (). reduser (0, a / del + b / del) 

Dette vil fungere, så lenge deler variabel er ikke null. Men hvis det er null, redusere() vil kaste en Aritmetisk unntak unntak: divider med null.

Vi kan enkelt fange unntaket og gjøre noe nyttig med det, for eksempel å logge det, gjenopprette fra det og så videre, avhengig av brukssaken, ved å bruke en prøve / fangst-blokk:

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 unntak: Divisjon etter null");} return 0;}); }

Selv om denne tilnærmingen vil fungere, vi forurenset lambdauttrykket med prøve / fange blokkere. Vi har ikke lenger den rene enlinjeren vi hadde før.

For å fikse dette problemet kan vi bruke teknikken for refactoring av ekstraktfunksjonen, og trekk ut prøve / fange blokkere i en egen metode:

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

Nå, implementeringen av divideListElements () metoden er igjen ren og strømlinjeformet:

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

Antar at divideListElements () er en verktøymetode implementert av et abstrakt NumberUtils klasse, kan vi lage en enhetstest for å kontrollere oppførselen til divideListElements () metode:

Listetall = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (tall, 1)). erEqualTo (21); 

La oss også teste divideListElements () metoden, når den leveres Liste av Heltall verdier inneholder et 0:

Listetall = Arrays.asList (0, 1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (tall, 1)). erEqualTo (21); 

Til slutt, la oss teste metodeimplementeringen når skillelinjen er 0 også:

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

6. Komplekse egendefinerte objekter

Vi kan også bruke Stream.reduce () med egendefinerte objekter som inneholder ikke-primitive felt. For å gjøre det, må vi gi et relevant itannhelse, akkumulator, og kombinator for datatypen.

Anta at vår Bruker er en del av et nettsted for gjennomgang. Hver av våre Brukers kan ha en Vurdering, som er gjennomsnitt over mange Anmeldelses.

Først, la oss starte med vår Anmeldelse gjenstand. Hver Anmeldelse skal inneholde en enkel kommentar og poengsum:

offentlig klasse gjennomgang {private int poeng; privat strenganmeldelse; // constructor, getters and setters}

Deretter må vi definere vår Vurdering, som vil holde våre anmeldelser sammen med en poeng felt. Når vi legger til flere anmeldelser, vil dette feltet øke eller redusere tilsvarende:

Offentlig klassevurdering {doble poeng; Listeomtaler = ny ArrayList (); public void add (Review review) {reviews.add (review); computeRating (); } privat dobbel computeRating () {dobbelt totalpoeng = anmeldelser.strøm (). kart (gjennomgang :: getPoints) .redusere (0, heltal :: sum); this.points = totalPoints / reviews.size (); returner dette. poeng; } offentlig statisk vurdering gjennomsnitt (vurdering r1, vurdering R2) {vurdering kombinert = ny vurdering (); combined.reviews = new ArrayList (r1.reviews); combined.reviews.addAll (r2.reviews); combined.computeRating (); retur kombinert; }}

Vi har også lagt til en gjennomsnitt funksjon for å beregne et gjennomsnitt basert på de to inngangene Vurderings. Dette vil fungere fint for oss kombinator og akkumulator komponenter.

Deretter la oss definere en liste over Brukers, hver med sine egne sett med anmeldelser.

User john = new User ("John", 30); john.getRating (). legg til (ny anmeldelse (5, "")); john.getRating (). legg til (ny anmeldelse (3, "ikke dårlig")); Bruker julie = ny bruker ("Julie", 35); john.getRating (). legg til (ny anmeldelse (4, "flott!")); john.getRating (). legg til (ny anmeldelse (2, "forferdelig opplevelse")); john.getRating (). legg til (ny anmeldelse (4, "")); Liste brukere = Arrays.asList (john, julie); 

Nå som John og Julie er regnskapsført, la oss bruke Stream.reduce () for å beregne en gjennomsnittlig vurdering på tvers av begge brukerne. Som en identitet, la oss returnere en ny Vurdering hvis inngangslisten vår er tom:

Vurdering av gjennomsnittlig vurdering = brukere.strøm (). Reduserer (ny vurdering (), (vurdering, bruker) -> Vurdering. gjennomsnitt (vurdering, bruker.getRating ()), vurdering :: gjennomsnitt);

Hvis vi gjør matte, bør vi oppdage at gjennomsnittlig poengsum er 3,6:

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

7. Konklusjon

I denne veiledningen, vi lærte å bruke Stream.reduce () operasjon. I tillegg lærte vi hvordan vi kan utføre reduksjoner på sekvensielle og parallelliserte strømmer, og hvordan vi kan håndtere unntak mens du reduserer.

Som vanlig er alle kodeeksemplene som vises i denne opplæringen tilgjengelig på GitHub.