Vodič za Stream.reduce ()

1. Pregled

API Stream pruža bogat repertoar posrednih, redukcijskih i terminalnih funkcija, koji također podržavaju paralelizaciju.

Točnije, operacije smanjenja toka omogućuju nam da proizvedemo jedan rezultat iz niza elemenata, primjenom opetovanog kombiniranja na elemente u nizu.

U ovom vodiču, osvrnut ćemo se na opću namjenu Stream.reduce () operacija i vidjeti u nekim konkretnim slučajevima upotrebe.

2. Ključni pojmovi: identitet, akumulator i kombinacija

Prije nego što dublje razmotrimo upotrebu Stream.reduce () raščlanimo elemente sudionika u operaciji u zasebne blokove. Na taj ćemo način lakše razumjeti ulogu koju svaki od njih igra:

  • Identitet - element koji je početna vrijednost operacije smanjenja i zadani rezultat ako je tok prazan
  • Akumulator - funkcija koja uzima dva parametra: djelomični rezultat operacije smanjenja i sljedeći element toka
  • Kombinator - funkcija koja se koristi za kombiniranje djelomičnog rezultata operacije smanjenja kada je redukcija paralelizirana ili kada postoji neusklađenost između vrsta argumenata akumulatora i tipova implementacije akumulatora

3. Korištenje Stream.reduce ()

Da bismo bolje razumjeli funkcionalnost elemenata identiteta, akumulatora i kombinacije, pogledajmo nekoliko osnovnih primjera:

Brojevi popisa = Arrays.asList (1, 2, 3, 4, 5, 6); int rezultat = brojevi .stream () .reduce (0, (subtotal, element) -> subtotal + element); assertThat (rezultat) .isEqualTo (21);

U ovom slučaju, the Cijeli broj vrijednost 0 je identitet. Pohranjuje početnu vrijednost operacije smanjenja, a također i zadani rezultat kada tok Cijeli broj vrijednosti je prazno.

Također, lambda izraz:

subtotal, element -> subtotal + element

je akumulator, budući da je potrebna djelomična suma od Cijeli broj vrijednosti i sljedeći element u toku.

Da bismo kôd učinili još sažetijim, možemo upotrijebiti referencu na metodu, umjesto lambda izraza:

int rezultat = numbers.stream (). reduce (0, Integer :: sum); assertThat (rezultat) .isEqualTo (21);

Naravno, možemo koristiti a smanjiti() rad na potocima koji drže druge vrste elemenata.

Na primjer, možemo koristiti smanjiti() na nizu od Niz elemente i spojite ih u jedan rezultat:

Slova s ​​popisa = Arrays.asList ("a", "b", "c", "d", "e"); Rezultat niza = slova .stream () .reduce ("", (djelomičniString, element) -> djelomičniString + element); assertThat (rezultat) .isEqualTo ("abcde");

Slično tome, možemo se prebaciti na verziju koja koristi referencu metode:

Rezultat niza = letters.stream (). Reduce ("", String :: concat); assertThat (rezultat) .isEqualTo ("abcde");

Iskoristimo smanjiti() operacija spajanja velikih slova elemenata slova niz:

Rezultat niza = slova .stream () .reduce ("", (djelomičniString, element) -> djelomičniString.toUpperCase () + element.toUpperCase ()); assertThat (rezultat) .isEqualTo ("ABCDE");

Osim toga, možemo koristiti smanjiti() u paraleliziranom toku (više o tome kasnije):

Popis dobnih skupina = Arrays.asList (25, 30, 45, 28, 32); int computedAges = age.parallelStream (). reduce (0, a, b -> a + b, Integer :: sum);

Kada se tok paralelno izvršava, Java runtime razdvaja tok na više podstruja. U takvim slučajevima, trebamo koristiti funkciju za kombiniranje rezultata podstruja u jednu. To je uloga kombinirača - u gornjem isječku to je Cijeli broj :: zbroj referenca metode.

Smiješno, ovaj se kod neće kompajlirati:

Popis korisnika = Arrays.asList (novi korisnik ("John", 30), novi korisnik ("Julie", 35)); int computedAges = users.stream (). reduce (0, (djelomičniAgeResult, korisnik) -> parcijalniAgeResult + user.getAge ()); 

U ovom slučaju imamo tok Korisnik objekti, a vrste argumenata akumulatora su Cijeli broj i Korisnik. Međutim, implementacija akumulatora zbroj je Cijeli brojevi, tako da kompajler jednostavno ne može zaključiti vrstu korisnik parametar.

Ovaj problem možemo riješiti pomoću kombinirača:

int rezultat = users.stream () .reduce (0, (djelomičniAgeResult, korisnik) -> djelomičniAgeResult + user.getAge (), Integer :: sum); assertThat (rezultat) .isEqualTo (65);

Pojednostavljeno, ako koristimo sekvencijalne tokove i podudaraju se tipovi argumenata akumulatora i vrste njegove implementacije, ne trebamo koristiti kombinirač.

4. Paralelno smanjenje

Kao što smo prije naučili, možemo koristiti smanjiti() na paraleliziranim potocima.

Kad koristimo paralelizirane tokove, to bismo trebali osigurati smanjiti() ili bilo koje druge skupne operacije izvršene na streamovima su:

  • asocijativni: redoslijed operanda ne utječe na rezultat
  • koji se ne miješaju: operacija ne utječe na izvor podataka
  • apatrid i deterministički: operacija nema stanje i daje isti izlaz za zadani ulaz

Morali bismo ispuniti sve ove uvjete kako bismo spriječili nepredvidive rezultate.

Kao što se i očekivalo, operacije izvedene na paraleliziranim strujama, uključujući smanjiti(), izvode se paralelno, stoga iskorištavajući višejezgrene hardverske arhitekture.

Iz očitih razloga, paralelizirani tokovi mnogo su učinkovitiji od sekvencijalnih kolega. Uprkos tome, mogu biti pretjerani ako operacije primijenjene na streamu nisu skupe ili je broj elemenata u streamu mali.

Naravno, paralelizirani tokovi pravi su put kada trebamo raditi s velikim tokovima i izvoditi skupe skupne operacije.

Stvorimo jednostavan JMH (Java Microbenchmark Harness) referentni test i usporedimo odgovarajuća vremena izvršavanja kada koristimo smanjiti() operacija na sekvencijalnom i paraleliziranom toku:

@State (Scope.Thread) privatni konačni popis userList = createUsers (); @Benchmark public Integer executeReduceOnParallelizedStream () {return this.userList .parallelStream () .reduce (0, (djelomičniRazultat, korisnik) -> djelomičniAgeResult + user.getAge (), Integer :: sum); } @Benchmark public Integer executeReduceOnSequentialStream () {return this.userList .stream () .reduce (0, (djelomičniAgeResult, korisnik) -> djelomičniAgeResult + user.getAge (), Integer :: sum); } 

U gornjoj JMH referentnoj vrijednosti uspoređujemo prosječna vremena izvršavanja. Jednostavno stvaramo Popis koji sadrže velik broj Korisnik predmeta. Dalje, zovemo smanjiti() na sekvencijalnom i paraleliziranom toku i provjerite radi li potonji brže od prvog (u sekundama po operaciji).

Ovo su naši referentni rezultati:

Jedinice pogrešaka rezultata ocjene Cnt JMHStreamReduceBenchMark.executeReduceOnParallelizedStream prosjek 5 0,007 ± 0,001 s / op JMHStreamReduceBenchMark.executeReduceOnSequentialStream prosjek 5 0,010 ± 0,001 s / 0,010 ± 0,001 s

5. Bacanje i rukovanje iznimkama uz smanjenje

U gornjim primjerima, smanjiti() operacija ne donosi nikakve iznimke. Ali moglo bi, naravno.

Na primjer, recimo da moramo podijeliti sve elemente toka s navedenim faktorom, a zatim ih zbrojiti:

Brojevi popisa = Arrays.asList (1, 2, 3, 4, 5, 6); int razdjelnik = 2; int rezultat = brojevi.stream (). smanjiti (0, a / razdjelnik + b / razdjelnik); 

Ovo će raditi dok god šestar varijabla nije nula. Ali ako je nula, smanjiti() bacit će ArithmeticException iznimka: podijeliti s nulom.

Iznimku možemo lako uhvatiti i učiniti nešto korisno s njom, kao što je bilježenje, oporavak od nje i tako dalje, ovisno o slučaju korištenja, pomoću bloka try / catch:

javni statički int divideListElements (Vrijednosti popisa, int razdjelnik) {return values.stream () .reduce (0, (a, b) -> {try {return a / djelilac + b / razdjelnik;} catch (ArithmeticException e) {LOGGER .log (Level.INFO, "Aritmetička iznimka: Podjela po nuli");} return 0;}); }

Iako će ovaj pristup funkcionirati, zagađivali smo lambda izraz s pokušaj uhvatiti blok. Više nemamo čistu jednostruku liniju kakvu smo imali prije.

Da bismo riješili ovaj problem, možemo se poslužiti tehnikom refaktoriranja funkcije izdvajanjai izvadite pokušaj uhvatiti blok u zasebnu metodu:

privatna statička int podjela (int vrijednost, int faktor) {int rezultat = 0; probajte {rezultat = vrijednost / faktor; } catch (ArithmeticException e) {LOGGER.log (Level.INFO, "Arithmetic Exception: Podjela po nuli"); } vratiti rezultat} 

Sada je provedba divideListElements () metoda je ponovno čista i pojednostavljena:

javni statički int divideListElements (Vrijednosti popisa, int razdjelnik) {return values.stream (). reduce (0, (a, b) -> podijeli (a, razdjelnik) + podijeli (b, razdjelnik)); } 

Pod pretpostavkom da divideListElements () je korisna metoda implementirana sažetkom NumberUtils klase, možemo stvoriti jedinstveni test za provjeru ponašanja divideListElements () metoda:

Brojevi popisa = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (brojevi, 1)). isEqualTo (21); 

Isprobajmo i divideListElements () metoda, kada se isporučuje Popis od Cijeli broj vrijednosti sadrži 0:

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

Na kraju, testirajmo i implementaciju metode kada je djelitelj 0:

Brojevi popisa = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (brojevi, 0)). isEqualTo (0);

6. Složeni prilagođeni objekti

Mi također mogu koristiti Stream.reduce () s prilagođenim objektima koji sadrže neprimitivna polja. Da bismo to učinili, trebamo pružiti odgovarajući izub, akumulator, i kombinirač za tip podataka.

Pretpostavimo naš Korisnik dio je web stranice s recenzijama. Svaka naša Korisnikmogu posjedovati jedan Ocjena, koja je u prosjeku za mnoge Pregleds.

Prvo, krenimo s našim Pregled objekt. Svaki Pregled treba sadržavati jednostavan komentar i ocjenu:

pregled javne klase {private int points; privatni pregled niza; // konstruktor, getteri i postavljači}

Dalje, moramo definirati naše Ocjena, koji će naše recenzije držati uz a bodova polje. Kako dodajemo još recenzija, ovo će se polje povećavati ili smanjivati ​​u skladu s tim:

ocjena javne klase {dvostruki bodovi; Popis recenzija = novi ArrayList (); public void add (Recenzija recenzije) {reviews.add (recenzija); computeRating (); } private double computeRating () {double totalPoints = reviews.stream (). map (Review :: getPoints) .reduce (0, Integer :: sum); this.points = totalPoints / reviews.size (); vrati this.points; } javni statički prosjek ocjene (Ocjena r1, Ocjena r2) {Ocjena kombinirana = nova Ocjena (); kombinirani.reviews = novi ArrayList (r1.reviews); kombinirani.reviews.addAll (r2.reviews); kombinirani.computeRating (); povratak kombiniran; }}

Također smo dodali prosječno funkcija za izračunavanje prosjeka na temelju dva ulaza Ocjenas. Ovo će lijepo raditi za naše kombinirač i akumulator komponente.

Dalje, definirajmo popis Korisniks, svaka sa svojim nizom recenzija.

Korisnik john = novi korisnik ("John", 30); john.getRating (). add (nova recenzija (5, "")); john.getRating (). add (nova recenzija (3, "nije loše")); Korisnik julie = novi korisnik ("Julie", 35); john.getRating (). add (nova recenzija (4, "sjajno!")); john.getRating (). add (nova recenzija (2, "užasno iskustvo")); john.getRating (). add (nova recenzija (4, "")); Popis korisnika = Arrays.asList (john, julie); 

Sad kad se računaju John i Julie, poslužimo se Stream.reduce () izračunati prosječnu ocjenu za oba korisnika. Kao an identitet, vratimo novu Ocjena ako je naša lista unosa prazna:

Ocjena prosječna ocjena = users.stream () .reduce (nova Ocjena (), (ocjena, korisnik) -> Prosjek ocjena (ocjena, user.getRating ()), Ocjena :: prosjek);

Ako radimo matematiku, trebali bismo utvrditi da je prosječna ocjena 3,6:

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

7. Zaključak

U ovom vodiču, naučili smo kako koristiti Stream.reduce () operacija. Osim toga, naučili smo kako izvoditi redukcije na sekvencijalnim i paraleliziranim tokovima te kako postupati s iznimkama uz smanjenje.

Kao i obično, svi uzorci koda prikazani u ovom vodiču dostupni su na GitHubu.