Sprievodca po Stream.reduce ()

1. Prehľad

Stream API poskytuje bohatý repertoár prechodných, redukčných a terminálových funkcií, ktoré tiež podporujú paralelizáciu.

Konkrétnejšie, operácie redukčného toku nám umožňujú vyprodukovať jeden jediný výsledok zo sledu prvkovopakovanou aplikáciou kombinujúcej operácie na prvky v poradí.

V tomto návode pozrieme sa na všeobecné účely Stream.reduce () prevádzka a uvidíte to v niektorých konkrétnych prípadoch použitia.

2. Kľúčové pojmy: identita, akumulátor a kombinátor

Než sa pozrieme hlbšie na používanie Stream.reduce () operáciu, poďme rozdeliť účastnícke prvky operácie do samostatných blokov. Takto ľahšie pochopíme rolu, ktorú hrá každý z nich:

  • Identita - prvok, ktorý je počiatočnou hodnotou operácie redukcie a predvoleným výsledkom, ak je prúd prázdny
  • Akumulátor - funkcia, ktorá prijíma dva parametre: čiastočný výsledok redukčnej operácie a ďalší prvok toku
  • Kombinátor - funkcia používaná na kombináciu čiastočného výsledku operácie redukcie, keď je redukcia paralelná alebo keď existuje nesúlad medzi typmi argumentov akumulátora a typmi implementácie akumulátora

3. Používanie Stream.reduce ()

Aby sme lepšie pochopili funkčnosť prvkov identity, akumulátora a kombinátora, pozrime sa na niekoľko základných príkladov:

Zoznam čísel = Arrays.asList (1, 2, 3, 4, 5, 6); int výsledok = čísla .stream () .reduce (0, (medzisúčet, prvok) -> medzisúčet + prvok); assertThat (výsledok) .isEqualTo (21);

V tomto prípade, the Celé číslo hodnota 0 je identita. Ukladá počiatočnú hodnotu operácie redukcie a tiež predvolený výsledok pri toku Celé číslo hodnoty sú prázdne.

Podobne, výraz lambda:

medzisúčet, prvok -> medzisúčet + prvok

je akumulátor, pretože trvá čiastočný súčet Celé číslo hodnoty a nasledujúci prvok v streame.

Aby bol kód ešte stručnejší, môžeme namiesto výrazu lambda použiť odkaz na metódu:

int výsledok = numbers.stream (). redukovať (0, Integer :: suma); assertThat (výsledok) .isEqualTo (21);

Samozrejme môžeme použiť a znížiť () prevádzka na prúdoch obsahujúcich iné typy prvkov.

Napríklad môžeme použiť znížiť () na pole String prvkov a spojiť ich do jedného výsledku:

Zoznam písmen = Arrays.asList ("a", "b", "c", "d", "e"); Výsledok reťazca = písmená .stream () .reduce ("", (čiastočný reťazec, prvok) -> čiastočný reťazec + prvok); assertThat (result) .isEqualTo ("abcde");

Podobne môžeme prejsť na verziu, ktorá používa odkaz na metódu:

Výsledok reťazca = letters.stream (). Redukovať ("", String :: concat); assertThat (výsledok) .isEqualTo ("abcde");

Použime znížiť () operácia na spájanie prvkov s veľkým písmenom písmená pole:

Výsledok reťazca = písmená .stream () .reduce ("", (partialString, element) -> partialString.toUpperCase () + element.toUpperCase ()); assertThat (výsledok) .isEqualTo ("ABCDE");

Okrem toho môžeme použiť znížiť () v paralelnom toku (viac o tom neskôr):

Zoznam vekových skupín = Arrays.asList (25, 30, 45, 28, 32); int computedAges = age.parallelStream (). redukovať (0, a, b -> a + b, Integer :: sum);

Keď sa tok vykonáva paralelne, runtime Java tento prúd rozdelí na viac podprúdov. V takých prípadoch, musíme použiť funkciu na spojenie výsledkov čiastkových streamov do jedného. Toto je úloha kombinátora - vo vyššie uvedenom úryvku je to Celé číslo :: súčet odkaz na metódu.

Je dosť vtipné, že sa tento kód nebude kompilovať:

Zoznam používateľov = Arrays.asList (nový používateľ ("John", 30), nový používateľ ("Julie", 35)); int computedAges = users.stream (). znížiť (0, (partialAgeResult, užívateľ) -> partialAgeResult + user.getAge ()); 

V tomto prípade máme prúd Používateľ objekty a typy argumentov akumulátora sú Celé číslo a Používateľ. Implementácia akumulátora je však súčtom Celé čísla, takže prekladač jednoducho nemôže odvodiť typ súboru používateľ parameter.

Tento problém môžeme vyriešiť pomocou kombinátora:

int výsledok = users.stream () .reduce (0, (partialAgeResult, užívateľ) -> partialAgeResult + user.getAge (), Integer :: suma); assertThat (výsledok) .isEqualTo (65);

Zjednodušene povedané, ak používame postupné toky a typy argumentov akumulátora a typy jeho implementácie sa zhodujú, nemusíme používať kombinátor.

4. Paralelné znižovanie

Ako sme sa už predtým dozvedeli, môžeme použiť znížiť () na paralelných tokoch.

Keď používame paralelizované toky, mali by sme sa o to uistiť znížiť () alebo akékoľvek ďalšie agregované operácie vykonávané v prúdoch sú:

  • asociatívny: výsledok nie je ovplyvnený poradím operandov
  • nezasahujúci: operácia nemá vplyv na zdroj údajov
  • bezstavový a deterministický: operácia nemá stav a produkuje rovnaký výstup pre daný vstup

Všetky tieto podmienky by sme mali splniť, aby sme zabránili nepredvídateľným výsledkom.

Podľa očakávania boli operácie vykonávané na paralelných tokoch, vrátane znížiť (), sa vykonávajú paralelne, a teda využívajú výhody viacjadrových hardvérových architektúr.

Zo zrejmých dôvodov paralelizované toky sú oveľa výkonnejšie ako sekvenčné náprotivky. Aj napriek tomu môžu byť nadmerné, ak operácie aplikované na stream nie sú drahé alebo je počet prvkov v streame malý.

Paralelizované prúdy sú samozrejme tou správnou cestou, keď potrebujeme pracovať s veľkými prúdmi a vykonávať drahé agregované operácie.

Vytvorme jednoduchý porovnávací test JMH (Java Microbenchmark Harness) a porovnajme príslušné časy vykonávania pri použití znížiť () prevádzka na postupnom a paralelnom toku:

@State (Scope.Thread) súkromný konečný zoznam 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); } 

Vo vyššie uvedenom benchmarku JMH porovnávame priemerné časy vykonania. Jednoducho vytvoríme a Zoznam obsahujúce veľké množstvo Používateľ predmety. Ďalej voláme znížiť () na sekvenčnom a paralelnom prúde a skontrolujte, či druhý pracuje rýchlejšie ako prvý (v sekundách na operáciu).

Toto sú naše referenčné výsledky:

Benchmark Mode Cnt Score Error Units JMHStreamReduceBenchMark.executeReduceOnParallelizedStream priem. 5 0,007 ± 0,001 s / op JMHStreamReduceBenchMark.executeReduceOnSequentialStream priem. 5 0,010 ± 0,001 s / op

5. Vrhanie a vybavovanie výnimiek pri znižovaní

Vo vyššie uvedených príkladoch je znížiť () prevádzka nevyvoláva žiadne výnimky. Ale to by samozrejme mohlo.

Povedzme napríklad, že musíme vydeliť všetky prvky toku dodávaným faktorom a potom ich sčítať:

Zoznam čísel = Arrays.asList (1, 2, 3, 4, 5, 6); delič int = 2; int výsledok = počty.prúd (). zmenšiť (0, a / delič + b / delič); 

Toto bude fungovať, pokiaľ rozdeľovač premenná nie je nula. Ale ak je nula, znížiť () bude hádzať Aritmetická výnimka výnimka: vydelíme nulou.

Výnimku môžeme ľahko chytiť a urobiť s ňou niečo užitočné, napríklad prihlásenie, zotavenie sa z nej a podobne, v závislosti od prípadu použitia, pomocou bloku try / catch:

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, "Aritmetická výnimka: Delenie nulou");} návrat 0;}); }

Aj keď tento prístup bude fungovať, znečistili sme výraz lambda znakom Skús chytiť blokovať. Už nemáme čistú jednoradovú vložku, ktorú sme mali predtým.

Na vyriešenie tohto problému môžeme použiť techniku ​​refaktoringu funkcie extraktua extrahovať Skús chytiť blokovať do samostatnej metódy:

private static int divide (int value, int factor) {int result = 0; skúsiť {výsledok = hodnota / faktor; } catch (ArithmeticException e) {LOGGER.log (Level.INFO, "Arithmetic Exception: Division by Zero"); } vrátiť výsledok} 

Teraz je implementácia divideListElements () metóda je opäť čistá a efektívna:

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

Za predpokladu, že divideListElements () je úžitková metóda implementovaná abstraktom NumberUtils triedy, môžeme vytvoriť test jednotky na kontrolu správania súboru divideListElements () metóda:

Zoznam čísel = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (numbers, 1)). isEqualTo (21); 

Poďme tiež otestovať divideListElements () metóda, ak sú dodávané Zoznam z Celé číslo hodnoty obsahuje 0:

Zoznam čísel = Arrays.asList (0, 1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (numbers, 1)). isEqualTo (21); 

Na záver otestujme implementáciu metódy, keď je delič tiež 0:

Zoznam čísel = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (čísla, 0)). isEqualTo (0);

6. Zložité vlastné objekty

My môže tiež použiť Stream.reduce () s vlastnými objektmi, ktoré obsahujú neprimitívne polia. Aby sme to mohli urobiť, musíme poskytnúť príslušné identita, akumulátor, a kombinátor pre dátový typ.

Predpokladajme, že náš Používateľ je súčasťou recenzného webu. Každý náš Používateľmôže jednu vlastniť Hodnotenie, čo je priemer mnohých Preskúmanies.

Najprv začnime s našimi Preskúmanie objekt. Každý Preskúmanie by mal obsahovať jednoduchý komentár a skóre:

public class Review {private int points; súkromná kontrola reťazcov; // konštruktor, getri a nastavovatelia}

Ďalej musíme definovať naše Hodnotenie, ktoré budú mať naše recenzie po boku a bodov lúka. Keď pridáme ďalšie recenzie, toto pole sa primerane zvýši alebo zníži:

verejná trieda Hodnotenie {dvojité body; Recenzie zoznamu = nový ArrayList (); public void add (recenzia na preskúmanie) {reviews.add (recenzia); computeRating (); } private double computeRating () {double totalPoints = reviews.stream (). map (Review :: getPoints) .reduce (0, Integer :: sum); this.points = totalPoints / reviews.size (); vrátiť toto.bodov; } public static Priemer priemeru (Hodnotenie r1, Hodnotenie r2) {Kombinované hodnotenie = nové Hodnotenie (); combined.reviews = new ArrayList (r1.reviews); combined.reviews.addAll (r2.reviews); combined.computeRating (); návrat kombinovaný; }}

Pridali sme tiež priemer funkcia na výpočet priemeru na základe dvoch vstupov Hodnotenies. Toto bude fungovať dobre pre naše kombinátor a akumulátor komponenty.

Ďalej definujeme zoznam Používateľs, každý s vlastnými súbormi recenzií.

Používateľ john = nový používateľ ("John", 30); john.getRating (). add (nová recenzia (5, "")); john.getRating (). add (nová recenzia (3, „nie zlé“)); Používateľ julie = nový používateľ ("Julie", 35); john.getRating (). add (nová recenzia (4, „skvelé!“)); john.getRating (). add (nová recenzia (2, „hrozná skúsenosť“)); john.getRating (). add (nová recenzia (4, "")); Zoznam používateľov = Arrays.asList (john, julie); 

Teraz, keď sa počíta s Johnom a Julie, použijeme to Stream.reduce () vypočítať priemerné hodnotenie u oboch používateľov. Ako identita, vráťme nový Hodnotenie ak je náš vstupný zoznam prázdny:

Rating averageRating = users.stream () .reduce (new Rating (), (hodnotenie, užívateľ) -> Rating.average (hodnotenie, user.getRating ()), hodnotenie :: priemer);

Ak urobíme matematiku, mali by sme zistiť, že priemerné skóre je 3,6:

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

7. Záver

V tomto návode naučili sme sa používať Stream.reduce () prevádzka. Ďalej sme sa naučili, ako vykonávať redukcie v postupných a paralelných prúdoch, a ako zvládať výnimky pri redukcii.

Ako obvykle sú všetky ukážky kódu zobrazené v tomto tutoriáli dostupné na GitHub.


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