Útmutató a Streamhez.reduce ()

1. Áttekintés

A Stream API gazdag köztes, redukciós és terminális funkciókkal rendelkezik, amelyek szintén támogatják a párhuzamosítást.

Pontosabban, redukciós folyam műveletek lehetővé teszik számunkra, hogy egyetlen eredményt hozzunk létre az elemek sorozatából, többszörös kombinációs műveletet alkalmazva a szekvencia elemeire.

Ebben az oktatóanyagban megnézzük az általános célokat Stream.reduce () művelet és nézze meg néhány konkrét felhasználási esetben.

2. A legfontosabb fogalmak: Identitás, Akkumulátor és Kombinátor

Mielőtt mélyebben megvizsgálnánk a Stream.reduce () művelet, bontsuk szét a művelet résztvevő elemeit külön blokkokra. Így könnyebben megértjük az egyes szerepeket:

  • Identitás - egy elem, amely a csökkentési művelet kezdeti értéke és az alapértelmezett eredmény, ha a folyam üres
  • Akkumulátor - egy olyan funkció, amely két paramétert vesz fel: a redukciós művelet részeredményét és a folyam következő elemét
  • Kombinátor - egy olyan funkció, amelyet a csökkentési művelet részeredményének kombinálásához használnak, amikor a redukció párhuzamos, vagy ha az akkumulátor argumentumok és az akkumulátor megvalósítás típusai között nincs eltérés

3. Használata Stream.reduce ()

Az identitás, az akkumulátor és az egyesítő elemek működésének jobb megértése érdekében nézzünk meg néhány alapvető példát:

Lista számok = tömbök. AsList (1, 2, 3, 4, 5, 6); int eredmény = számok .folyam () .csökkent (0, (részösszeg, elem) -> részösszeg + elem); assertThat (eredmény) .isEqualTo (21);

Ebben az esetben, a Egész szám a 0 érték az azonosság. Tárolja a redukciós művelet kezdeti értékét, valamint az alapértelmezett eredményt, amikor a Egész szám értékek üresek.

Hasonlóképpen, a lambda kifejezés:

részösszeg, elem -> részösszeg + elem

az akkumulátor, mivel a részleges összege szükséges Egész szám értékeket és a patak következő elemét.

Annak érdekében, hogy a kód még tömörebb legyen, használhatunk metódus referenciát a lambda kifejezés helyett:

int eredmény = számok.folyam (). csökkentés (0, Egész szám :: összeg); assertThat (eredmény) .isEqualTo (21);

Természetesen használhatjuk az a csökkenteni () művelet más típusú elemeket tartó patakokon.

Például használhatjuk csökkenteni () tömbjén Húr elemeket, és egyesítse őket egyetlen eredményben:

Betűk felsorolása = Arrays.asList ("a", "b", "c", "d", "e"); Karakterlánc eredménye = betűk .stream () .reduce ("", (részlegesString, elem) -> részlegesString + elem); assertThat (eredmény) .isEqualTo ("abcde");

Hasonlóképpen átválthatunk a módszerreferenciát használó verzióra:

Karakterlánc eredménye = letter.stream (). Reduc ("", String :: concat); assertThat (eredmény) .isEqualTo ("abcde");

Használjuk a csökkenteni () művelet a felső ház elemeinek összekapcsolásához leveleket sor:

Karakterlánc eredménye = betűk .stream () .reduce ("", (részlegesString, elem) -> részlegesString.toUpperCase () + elem.toUpperCase ()); assertThat (eredmény) .isEqualTo ("ABCDE");

Ezen felül használhatjuk csökkenteni () párhuzamos folyamban (erről később)

Korosztályok listája = Arrays.asList (25, 30, 45, 28, 32); int computedAges = age.parallelStream (). csökkent (0, a, b -> a + b, Egész szám :: összeg);

Amikor egy adatfolyam párhuzamosan fut, a Java futásideje több alfolyamra osztja a folyamot. Ilyen esetekben, függvényt kell használnunk ahhoz, hogy az alfolyamok eredményeit egyetlen eggyé egyesítsük. Ez a kombinátor szerepe - a fenti részletben ez a Egész szám :: összeg módszer referencia.

Elég vicces, de ez a kód nem áll össze:

Felhasználók listája = Arrays.asList (új felhasználó ("John", 30), új felhasználó ("Julie", 35)); int computedAges = users.stream (). csökkenteni (0, (részlegesAgeResult, felhasználó) -> részlegesAgeResult + felhasználó.getAge ()); 

Ebben az esetben van egy adatfolyamunk Felhasználó objektumok, és az akkumulátor argumentumok típusai Egész szám és Felhasználó. Az akkumulátor megvalósítása azonban összege Egész számok, így a fordító nem tud következtetni a felhasználó paraméter.

Megoldhatjuk ezt a problémát egy kombinátor segítségével:

int eredmény = felhasználók.folyam () .reduce (0, (részlegesAgeResult, felhasználó) -> részlegesAgeResult + felhasználó.getAge (), Egész szám :: összeg); assertThat (eredmény) .isEqualTo (65);

Leegyszerűsítve: ha szekvenciális adatfolyamokat használunk, és az akkumulátor argumentumok típusai és a megvalósítás típusai egyeznek, akkor nem kell kombinátort használni.

4. Redukció párhuzamosan

Mint korábban megtudtuk, használhatjuk csökkenteni () párhuzamos folyamokon.

Párhuzamosított folyamok használatakor meg kell győződnünk arról csökkenteni () vagy a folyamokon végrehajtott bármely más összesített művelet:

  • asszociációs: az eredményt nem befolyásolja az operandusok sorrendje
  • nem zavaró: a művelet nem befolyásolja az adatforrást
  • hontalan és meghatározó: a műveletnek nincs állapota, és ugyanazt a kimenetet produkálja egy adott bemenethez

A kiszámíthatatlan eredmények megelőzése érdekében mindezeket a feltételeket teljesítenünk kell.

A várakozásoknak megfelelően a párhuzamos adatfolyamokon végrehajtott műveletek, beleértve a következőket: csökkenteni (), párhuzamosan kerülnek végrehajtásra, így kihasználva a többmagos hardverarchitektúrákat.

Nyilvánvaló okokból a párhuzamos áramok sokkal jobban teljesítenek, mint a szekvenciális partnerek. Ennek ellenére túlteljesek lehetnek, ha az adatfolyamra alkalmazott műveletek nem drágák, vagy ha az adatfolyamban kicsi az elemek száma.

Természetesen a párhuzamos adatfolyamok a megfelelő út, amikor nagy adatfolyamokkal kell dolgozni, és drága összesítési műveleteket kell végrehajtanunk.

Hozzunk létre egy egyszerű JMH (Java Microbenchmark Harness) tesztet, és hasonlítsuk össze a megfelelő végrehajtási időket a csökkenteni () művelet szekvenciális és párhuzamos áramban:

@State (Scope.Thread) privát végső lista userList = createUsers (); @Benchmark public Integer executeReduceOnParallelizedStream () {return this.userList .parallelStream () .reduce (0, (részlegesAgeResult, felhasználó) -> részlegesAgeResult + user.getAge (), Integer :: összeg); } @Benchmark public Integer executeReduceOnSequentialStream () {return this.userList .stream () .reduce (0, (partsAgeResult, user) -> részlegesAgeResult + user.getAge (), Integer :: összeg); } 

A fenti JMH benchmarkban összehasonlítjuk a végrehajtás átlagidejét. Egyszerűen létrehozunk egy Lista amelyek nagy számban tartalmaznak Felhasználó tárgyakat. Ezután felhívjuk csökkenteni () egy szekvenciális és párhuzamos áramon, és ellenőrizze, hogy az utóbbi gyorsabban teljesít-e, mint az előbbi (másodpercenként / műveletenként).

Ezek a benchmark eredményeink:

Benchmark Mode 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. Kivételek dobása és kezelése csökkentés közben

A fenti példákban a csökkenteni () a művelet nem vet ki kivételeket. De természetesen lehet.

Például mondjuk azt, hogy el kell osztanunk egy adatfolyam összes elemét egy megadott tényezővel, majd összegezzük őket:

Lista számok = tömbök. AsList (1, 2, 3, 4, 5, 6); int elválasztó = 2; int eredmény = számok.folyam (). redukció (0, a / osztó + b / osztó); 

Ez működni fog, amíg a osztó változó nem nulla. De ha nulla, csökkenteni () dobni fog egy Számtani kivétel kivétel: osztani nullával.

Könnyen elkaphatjuk a kivételt, és tehetünk vele valami hasznosat, például naplózhatunk, helyreállhatunk belőle és így tovább, a felhasználástól függően, egy try / catch blokk használatával:

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, "Számtani kivétel: osztás nullával");} return 0;}); }

Bár ez a megközelítés működni fog, szennyeztük a lambda kifejezést az próbáld / fogd Blokk. Már nincs meg az a tiszta egybélés, amely korábban volt.

A probléma megoldásához használhatjuk a kivonat funkció refaktorálási technikáját, és vonja ki a próbáld / fogd külön módszerbe blokkolja:

privát statikus int osztás (int érték, int tényező) {int eredmény = 0; próbáld ki az {eredmény = érték / tényező; } catch (ArithmeticException e) {LOGGER.log (Level.INFO, "Aritmetikai kivétel: osztás nullával"); } visszatérési eredmény} 

Most a divideListElements () a módszer ismét tiszta és korszerűsített:

public static int divideListElements (List value, int divider) {return values.stream (). reduc (0, (a, b) -> ossza (a, osztó) + oszt (oszt, oszt)); } 

Feltéve, hogy divideListElements () egy absztrakt segítségével megvalósított segédprogram NumberUtils osztályban, létrehozhatunk egy egység tesztet a divideListElements () módszer:

Lista számok = tömbök. AsList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (számok, 1)). isEqualTo (21); 

Teszteljük is a divideListElements () módszer, ha a mellékelt Lista nak,-nek Egész szám értékek 0-t tartalmaznak:

Lista számok = tömbök. AsList (0, 1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (számok, 1)). isEqualTo (21); 

Végül teszteljük a módszer megvalósítását, amikor az osztó értéke 0 is:

Lista számok = tömbök. AsList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (számok, 0)). isEqualTo (0);

6. Komplex egyéni objektumok

Mi használhatja is Stream.reduce () nem primitív mezőket tartalmazó egyedi objektumokkal. Ehhez meg kell adnunk egy releváns ifogászat, akkumulátor, és kombinátor az adattípushoz.

Tegyük fel, hogy a mi Felhasználó egy felülvizsgálati webhely része. Mindegyikünk Felhasználós birtokolhat egyet Értékelés, amelyet sokakra átlagolnak Felülvizsgálats.

Először kezdjük a sajátunkkal Felülvizsgálat tárgy. Minden egyes Felülvizsgálat tartalmaznia kell egy egyszerű megjegyzést és pontszámot:

nyilvános osztályszemle {private int points; privát húrszemle; // kivitelező, mérőeszközök és beállítók}

Ezután meg kell határoznunk a sajátunkat Értékelés, amely a felülvizsgálataink mellett a pontokat terület. További vélemények hozzáadásával ez a mező ennek megfelelően növekszik vagy csökken:

nyilvános osztály besorolása {dupla pont; Lista vélemények = új ArrayList (); public void add (Recenzió felülvizsgálata) {reviews.add (recenzió); computeRating (); } private double computeRating () {double totalPoints = reviews.stream (). map (Review :: getPoints) .reduce (0, Egész szám :: összeg); this.points = totalPoints / reviews.size (); adja vissza ezt.pontok; } nyilvános statikus besorolási átlag (r1 besorolás, r2 besorolás) {összesítés = új besorolás (); kombinált.reviews = új ArrayList (r1.reviews); kombinált.reviews.addAll (r2.reviews); combined.computeRating (); visszatérés együttesen; }}

Hozzáadtunk egy átlagos függvény az átlag kiszámításához a két bemenet alapján Értékeléss. Ez a mi hasznunkra lesz kombinátor és akkumulátor alkatrészek.

Ezután definiáljuk a listát Felhasználós, mindegyik saját vélemény-készlettel.

John = új felhasználó ("John", 30); john.getRating (). add (új áttekintés (5, "")); john.getRating (). add (új áttekintés (3, "nem rossz")); Felhasználó julie = új felhasználó ("Julie", 35); john.getRating (). add (új áttekintés (4, "nagyszerű!")); john.getRating (). add (new Review (2, "borzalmas élmény")); john.getRating (). add (új áttekintés (4, "")); Felhasználók listája = Arrays.asList (john, julie); 

Most, hogy John és Julie elszámolásra kerül, használjuk Stream.reduce () hogy kiszámítsa a felhasználók átlagát. Mint egy identitás, adjunk vissza egy újat Értékelés ha a beviteli listánk üres:

Rating averageRating = users.stream () .reduce (new Rating (), (rating, user) -> Rating.averation (rating, user.getRating ()), Rating :: átlagos);

Ha elvégezzük a matematikát, meg kell találnunk, hogy az átlagos pontszám 3,6:

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

7. Következtetés

Ebben az oktatóanyagban megtanultuk a Stream.reduce () művelet. Ezenkívül megtanultuk, hogyan kell elvégezni a szekvenciális és párhuzamos adatfolyamok redukcióit, és hogyan kell kezelni a kivételeket a csökkentés során.

Szokás szerint az ebben az oktatóanyagban bemutatott összes kódminta elérhető a GitHubon.