A Java 8 Stream API bemutatója

1. Áttekintés

Ebben a részletes bemutatóban áttekintjük a Java 8 Stream gyakorlati használatát a létrehozástól a párhuzamos végrehajtásig.

Az anyag megértéséhez az olvasóknak alapvető ismeretekkel kell rendelkezniük a Java 8-ról (lambda kifejezések, Választható, módszer referenciák) és a Stream API-t. Ha még nem ismeri ezeket a témákat, kérjük, olvassa el korábbi cikkeinket - Új szolgáltatások a Java 8-ban és Bevezetés a Java 8-adatfolyamokba.

2. Patak létrehozása

Sokféle módon hozhat létre különböző forrásokból álló adatfolyam-példányt. Miután létrehozta, a példány nem módosítja a forrását, ezért lehetővé teszi több példány létrehozását egyetlen forrásból.

2.1. Üres adatfolyam

A üres() metódust kell használni üres adatfolyam létrehozása esetén:

Stream streamEmpty = Stream.empty ();

Gyakran előfordul, hogy a üres() módszert használnak a létrehozáskor a visszatérés elkerülése érdekében nulla elem nélküli adatfolyamok esetében:

public Stream streamOf (List list) return list == null 

2.2. Patak Gyűjtemény

Bármilyen típusú stream létrehozható Gyűjtemény (Gyűjtemény, lista, készlet):

Gyűjteménygyűjtemény = Arrays.asList ("a", "b", "c"); Stream streamOfCollection = collection.stream ();

2.3. A tömb patakja

A tömb egy adatfolyam forrása is lehet:

Stream streamOfArray = Stream.of ("a", "b", "c");

Létrehozhatók egy meglévő tömbből vagy egy tömb részéből is:

Karakterlánc [] arr = új karakterlánc [] {"a", "b", "c"}; Stream streamOfArrayFull = Tömbök.stream (arr); Stream streamOfArrayPart = Arrays.stream (arr, 1, 3);

2.4. Stream.builder ()

Amikor építőt használnak a kívánt típust ezen felül meg kell adni az utasítás jobb oldalán, különben az épít() metódus létrehozza a Folyam:

Stream streamBuilder = Stream.builder (). Add ("a"). Add ("b"). Add ("c"). Build ();

2.5. Stream.generate ()

A generál() módszer elfogadja a Támogató elemgeneráláshoz. Mivel az így kapott adatfolyam végtelen, a fejlesztőnek meg kell adnia a kívánt méretet vagy generál() A módszer addig fog működni, amíg el nem éri a memóriahatárt:

Stream streamGenerated = Stream.generate (() -> "elem"). Korlát (10);

A fenti kód tíz karaktersorozatot hoz létre a következő értékkel: "elem".

2.6. Stream.iterate ()

A végtelen adatfolyam létrehozásának másik módja a hajtogat() módszer:

Stream streamIterated = Stream.iterate (40, n -> n + 2) .limit (20);

A kapott adatfolyam első eleme a hajtogat() módszer. Minden következő elem létrehozásához a megadott függvényt alkalmazzuk az előző elemre. A fenti példában a második elem 42 lesz.

2.7. A primitívek folyama

A Java 8 lehetőséget kínál három primitív streaming létrehozására: int, hosszú és kettős. Mint Folyam egy általános interfész, és a primitívek típusparamétereként nem használhatók az általánosokkal, három új speciális interfész jött létre: IntStream, LongStream, DoubleStream.

Az új interfészek használata megkönnyíti a felesleges autóbokszolást, ami növeli a termelékenységet:

IntStream intStream = IntStream.range (1, 3); LongStream longStream = LongStream.rangeClosed (1, 3);

A tartomány (int startInclusive, int endExclusive) metódus rendezett folyamot hoz létre az első paramétertől a második paraméterig. Ez növeli a következő elemek értékét az 1-gyel egyenlő lépéssel. Az eredmény nem tartalmazza az utolsó paramétert, csak a szekvencia felső határa.

A rangeClosed (int startInclusive, int endInclusive)metódus ugyanezt teszi, csak egy különbséggel - a második elem szerepel. Ez a két módszer felhasználható a primitívek háromféle típusának bármelyikének előállítására.

Java 8 óta a Véletlen osztály számos módszert kínál a primitívek generálásához. Például a következő kód létrehoz egy DoubleStream, amelynek három eleme van:

Véletlenszerű véletlenszerű = új Véletlenszerű (); DoubleStream doubleStream = random.doubles (3);

2.8. Patak Húr

Húr patak létrehozásának forrásaként is használható.

A segítségével karakterek () módszere Húr osztály. Mivel nincs interfész CharStream a JDK-ban az IntStream helyette a betűáramlat ábrázolására szolgál.

IntStream streamOfChars = "abc" .chars ();

A következő példa megszakítja a Húr rész-karakterláncokra a megadottak szerint RegEx:

Stream streamOfString = Pattern.compile (",") .splitAsStream ("a, b, c");

2.9. Fájlfolyam

Java NIO osztály Fájlok lehetővé teszi a Folyam egy szöveges fájlt a vonalak () módszer. A szöveg minden sora a stream elemévé válik:

Elérési útvonal = Paths.get ("C: \ file.txt"); Stream streamOfStrings = Files.lines (elérési út); Stream streamWithCharset = Files.lines (elérési út, Charset.forName ("UTF-8"));

A Charset argumentumaként határozható meg vonalak () módszer.

3. Hivatkozás egy adatfolyamra

Lehetőség van egy adatfolyam előállítására és hozzáférhető hivatkozásra, amennyiben csak közbenső műveleteket hívnak meg. A terminál művelet végrehajtása a folyamot elérhetetlenné teszi.

Ennek bemutatására egy ideig elfelejtjük, hogy a legjobb gyakorlat a műveletsorozat láncolása. A felesleges részletesség mellett technikailag a következő kód érvényes:

Stream stream = Stream.of ("a", "b", "c"). Szűrő (elem -> elem.tartalmaz ("b")); Opcionális anyElement = stream.findAny ();

De ha megpróbálja ugyanazon hivatkozást újból felhasználni a terminál műveletének meghívása után, akkor a IllegalStateException:

Opcionális firstElement = stream.findFirst ();

Mivel a IllegalStateException egy RuntimeException, a fordító nem jelez egy problémát. Tehát nagyon fontos erre emlékezni Java 8 a patakokat nem lehet újra felhasználni.

Ez a fajta viselkedés logikus, mert a folyamokat arra tervezték, hogy egy véges műveletsorozatot funkcionális stílusban alkalmazzanak az elemek forrására, de ne tároljanak elemeket.

Tehát az előző kód megfelelő működéséhez néhány változtatást kell végrehajtani:

Lista elemek = Stream.of ("a", "b", "c"). Szűrő (elem -> elem.tartalmaz ("b")) .collect (Collectors.toList ()); Opcionális anyElement = elements.stream (). FindAny (); Opcionális firstElement = elements.stream (). FindFirst ();

4. Patak csővezeték

Az adatforrás elemein végzett műveletsorozat végrehajtásához és eredményeik összesítéséhez három részre van szükség - a forrás, közbenső művelet (ek) és a terminál működése.

A köztes műveletek új módosított adatfolyamot adnak vissza. Például a meglévő új adatfolyamának létrehozása kevés elem nélkül a kihagy () módszert kell használni:

Stream onceModifiedStream = Stream.of ("abcd", "bbcd", "cbcd"). Skip (1);

Ha egynél több módosításra van szükség, a köztes műveletek láncolhatók. Tegyük fel, hogy az áram minden elemét is helyettesítenünk kell Folyam az első néhány karakterből álló vonósorral. Ez a kihagy () és a térkép() mód:

Stream kétszerModifiedStream = stream.skip (1) .map (element -> element.substring (0, 3));

Mint láthatja, a térkép() A metódus paraméterként lambda kifejezést vesz fel. Ha többet szeretne megtudni a lambdákról, nézze meg a Lambda kifejezések és funkcionális interfészek oktatóanyagunkat: Tippek és legjobb gyakorlatok.

Egy adatfolyam önmagában nem ér semmit, a valódi dolog, ami a felhasználót érdekli, a terminál működésének eredménye, amely lehet valamilyen típusú érték vagy egy művelet, amelyet a stream minden elemére alkalmaznak. Csak egy terminál művelet használható streamenként.

A streamek használatának helyes és legkényelmesebb módja a folyamvezeték, amely az áramforrás, a közbenső műveletek és a terminálműveletek láncolata. Például:

List list = Arrays.asList ("abc1", "abc2", "abc3"); hosszú méret = list.stream (). skip (1) .map (element -> element.substring (0, 3)). sorted (). count ();

5. Lusta hívás

A közbenső műveletek lusták. Ez azt jelenti csak akkor hívják meg őket, ha ez szükséges a terminál művelet végrehajtásához.

Ennek bemutatásához képzelje el, hogy van módszerünk felhívták(), amely növeli a belső számlálót minden alkalommal, amikor hívják:

hosszú magánpult; private void wasCalled () {számláló ++; }

Hívjuk a metódustHívott() működéstől szűrő():

Lista lista = Arrays.asList („abc1”, „abc2”, „abc3”); számláló = 0; Stream stream = list.stream (). Filter (elem -> {wasCalled (); return element.contains ("2");});

Mivel három elemből áll a forrásunk, feltételezhetjük ezt a módszert szűrő() háromszor lesz meghívva, és a számláló változó 3. A kód futtatása azonban nem változik számláló egyáltalán, még mindig nulla, tehát a szűrő() módszert még egyszer sem hívták meg. Ennek oka - hiányzik a terminál működéséből.

Írjuk át ezt a kódot egy kicsit a hozzáadásával térkép() üzemeltetés és terminál működés - findFirst (). Hozzáadunk egy lehetőséget a módszerhívások sorrendjének nyomon követésére naplózás segítségével:

Választható stream = list.stream (). Filter (elem -> {log.info ("a filter () nevet" hívták); return element.contains ("2");}). Map (element -> {log.info ("a map () nevet kapta"); return elem.toUpperCase ();}). findFirst ();

Az eredménynapló azt mutatja, hogy a szűrő() metódust kétszer hívták meg és a térkép() módszer csak egyszer. Ez azért van így, mert a csővezeték függőlegesen hajt végre. Példánkban a stream első eleme nem felelt meg a szűrő predikátumának, majd a szűrő() metódust hívták meg a második elemre, amely megfelelt a szűrőnek. A telefon hívása nélkül szűrő() a harmadik elemnél csővezetéken keresztül mentünk le a térkép() módszer.

A findFirst () a művelet csak egy elemmel elégít ki. Tehát ebben a konkrét példában a lusta invokáció lehetővé tette két módszerhívás elkerülését - egyet a szűrő() és egyet a térkép().

6. A végrehajtás rendje

A teljesítmény szempontjából a helyes sorrend az egyik legfontosabb szempont a láncolás műveleteiben a folyamvezetékben:

hosszú méret = list.stream (). map (elem -> {wasCalled (); return elem.substring (0, 3);}). skip (2) .count ();

Ennek a kódnak a végrehajtása hárommal növeli a számláló értékét. Ez azt jelenti, hogy a térkép() módszer a patak hívták háromszor. De az értéke méret az egyik. Tehát a kapott adatfolyamnak csak egy eleme van, és mi végrehajtottuk a drágát térkép() műveleteket ok nélkül háromszor kétszer.

Ha megváltoztatjuk a kihagy () és a térkép() mód, a számláló csak eggyel növekszik. Szóval, a módszer térkép() csak egyszer hívják:

hosszú méret = list.stream (). skip (2) .map (elem -> {wasCalled (); return elem.substring (0, 3);}). count ();

Ez hozza fel a szabályt: a folyam méretét csökkentő közbenső műveleteket el kell helyezni az egyes elemekre alkalmazandó műveletek előtt. Tehát tartsa meg az olyan módszereket, mint az skip (), szűrő (), különálló () a patakvezeték tetején.

7. Patakcsökkentés

Az API-nak számos terminálművelete van, amelyek összesítik a folyamot egy típusra vagy egy primitívre, például count (), max (), min (), sum (), de ezek a műveletek az előre definiált megvalósításnak megfelelően működnek. És akkor ha egy fejlesztőnek testre kell szabnia a Stream csökkentési mechanizmusát? Két módszer engedi ezt megtenni - a csökkenteni ()és a gyűjt() mód.

7.1. A csökkenteni () Módszer

Ennek a módszernek három változata van, amelyek az aláírásuk és a visszatérő típusaik szerint különböznek egymástól. A következő paraméterek lehetnek:

identitás - egy akkumulátor kezdeti értéke vagy egy alapértelmezett érték, ha egy adatfolyam üres, és nincs mit felhalmozni;

akkumulátor - egy függvény, amely meghatározza az elemek összesítésének logikáját. Mivel az akkumulátor új értéket hoz létre a csökkentés minden lépéséhez, az új értékek mennyisége megegyezik a folyam méretével, és csak az utolsó érték hasznos. Ez nem túl jó az előadás szempontjából.

kombinátor - egy függvény, amely összesíti az akkumulátor eredményeit. A kombinátort csak párhuzamos módban hívják meg, hogy csökkentse a különböző szálakból származó akkumulátorok eredményeit.

Tehát nézzük meg ezt a három módszert működés közben:

OpcionálisInt csökkentett = IntStream.range (1, 4) .reduce ((a, b) -> a + b);

csökkent = 6 (1 + 2 + 3)

int reducTwoParams = IntStream.range (1, 4) .reduce (10, (a, b) -> a + b);

csökkentett KétParam = 16 (10 + 1 + 2 + 3)

int reducParams = Stream.of (1, 2, 3) .reduce (10, (a, b) -> a + b, (a, b) -> {log.info ("a kombinátort hívták"); adjon vissza egy + b;});

Az eredmény ugyanaz lesz, mint az előző példában (16), és nem lesz bejelentkezés, ami azt jelenti, hogy a kombinátort nem hívták meg. A kombinátor működéséhez az adatfolyamnak párhuzamosnak kell lennie:

int csökkentettPárhuzamos = Tömbök.asList (1, 2, 3) .parallelStream () .reduce (10, (a, b) -> a + b, (a, b) -> {log.info ("a kombinátort" adja vissza a + b;});

Az eredmény itt más (36), és a kombinátort kétszer hívták meg. Itt a redukció a következő algoritmus szerint működik: az akkumulátor háromszor futott, hozzáadva az adatfolyam minden elemét identitás a patak minden eleméhez. Ezeket a műveleteket párhuzamosan hajtják végre. Ennek eredményeként megvan (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;). Most a kombinátor egyesítheti ezt a három eredményt. Ehhez két iterációra van szükség (12 + 13 = 25; 25 + 11 = 36).

7.2. A gyűjt() Módszer

Az áramcsökkentést egy másik terminálművelettel - a gyűjt() módszer. Elfogad egy típusú argumentumot Gyűjtő, amely meghatározza a redukció mechanizmusát. A leggyakoribb műveletekhez már vannak előre definiált gyűjtők. A. Segítségével érhetők el Gyűjtők típus.

Ebben a részben a következőket fogjuk használni Lista forrásként az összes adatfolyamhoz:

ListList = Arrays.asList (új termék (23, "burgonya"), új termék (14, "narancs"), új termék (13, "citrom"), új termék (23, "kenyér"), új termék ( 13. "cukor"));

Patak konvertálása Gyűjtemény (Gyűjtemény, lista vagy Készlet):

List collectorCollection = productList.stream (). Map (Product :: getName) .collect (Collectors.toList ());

Csökkentés Húr:

String listToString = productList.stream (). Map (Product :: getName) .collect (Collectors.joining (",", "[", "]"));

A asztalos() A metódus 1-3 paramétert tartalmazhat (elválasztó, előtag, utótag). A legkényelmesebb dolog a használatban asztalos() - A fejlesztőnek nem kell ellenőriznie, hogy a folyam eléri-e a végét, hogy alkalmazza az utótagot, és ne alkalmazzon elválasztót. Gyűjtő gondoskodni fog arról.

Az adatfolyam összes numerikus elemének átlagos értékének feldolgozása:

dupla átlagár = productList.stream () .collect (Collectors.averagingInt (Product :: getPrice));

Az adatfolyam összes numerikus elemének összegének feldolgozása:

int summingPrice = productList.stream () .collect (Collectors.summingInt (Termék :: getPrice));

Mód átlagolásXX (), összegzésXX () és összesítőXX () úgy működhet, mint a primitívekkel (int, hosszú, dupla), mint a burkoló osztályaiknál ​​(Egész, hosszú, dupla). Ezeknek a módszereknek még egy hatalmas vonása a feltérképezés biztosítása. Tehát a fejlesztőnek nem kell kiegészítőt használnia térkép() művelet a gyűjt() módszer.

Statisztikai információk gyűjtése az adatfolyam elemeiről:

IntSummaryStatistics statistics = productList.stream () .collect (Collectors.summarizingInt (Termék :: getPrice));

A kapott típusú példány használatával IntSummaryStatistics a fejlesztő alkalmazással készíthet statisztikai jelentést toString () módszer. Az eredmény a Húr közös ebben „IntSummaryStatistics {count = 5, sum = 86, min = 13, átlag = 17.200000, max = 23}".

Könnyen kivonható az objektumból különálló értékek a szám, összeg, min, átlag módszerek alkalmazásával getCount (), getSum (), getMin (), getAverage (), getMax (). Mindezek az értékek egyetlen csővezetékből nyerhetők ki.

Az adatfolyam elemeinek csoportosítása a megadott függvény szerint:

Térkép collectorMapOfLists = productList.stream () .collect (Collectors.groupingBy (Product :: getPrice));

A fenti példában a folyam a Térkép amely az összes terméket áruk szerint csoportosítja.

A folyam elemeinek csoportosítása egyes állítmányok szerint:

Térkép mapPartioned = productList.stream () .collect (Collectors.partitioningBy (elem -> element.getPrice ()> 15));

A kollektor nyomása további átalakítás végrehajtására:

Set unmodifiableSet = productList.stream () .collect (Collectors.collectingAndThen (Collectors.toSet (), Gyűjtemények :: unmodifiableSet));

Ebben a konkrét esetben a gyűjtő egy folyamot alakított át a-vá Készlet majd létrehozta a módosíthatatlant Készlet kifelé ezzel.

Egyedi gyűjtő:

Ha valamilyen okból létre kell hozni egy egyedi gyűjtőt, akkor ennek legegyszerűbb és kevésbé bőbeszédű módja a módszer használata nak,-nek() típusú Gyűjtő.

Gyűjtő toLinkedList = Collector.of (LinkedList :: new, LinkedList :: add, (first, second) -> {first.addAll (second); return first;}); LinkedList linkedListOfPersons = productList.stream (). Collect (toLinkedList);

Ebben a példában a Gyűjtő lecsökkent a LinkedList.

Párhuzamos folyamok

A Java 8 előtt a párhuzamosítás bonyolult volt. A felbukkanó ExecutorService és a ForkJoin egy kicsit leegyszerűsítette a fejlesztő életét, de még mindig szem előtt kell tartaniuk, hogyan lehet létrehozni egy adott végrehajtót, hogyan kell futtatni és így tovább.A Java 8 bemutatta a párhuzamosság funkcionális stílusban történő megvalósításának módját.

Az API lehetővé teszi párhuzamos folyamok létrehozását, amelyek párhuzamos módban hajtanak végre műveleteket. Amikor egy adatfolyam forrása a Gyűjtemény vagy egy sor segítségével érhető el parallelStream () módszer:

Stream streamOfCollection = productList.parallelStream (); logikai isParallel = streamOfCollection.isParallel (); logikai bigPrice = streamOfCollection .map (termék -> product.getPrice () * 12) .anyMatch (ár -> ár> 200);

Ha az adatfolyam forrása valami más, mint a Gyűjtemény vagy egy sor, a párhuzamos() módszert kell használni:

IntStream intStreamParallel = IntStream.range (1, 150) .parallel (); logikai isParallel = intStreamParallel.isParallel ();

A fedél alatt a Stream API automatikusan a ForkJoin a műveletek párhuzamos végrehajtására. Alapértelmezés szerint a közös szálkészlet lesz használva, és nincs mód (legalábbis egyelőre) hozzá rendelni néhány egyéni szálkészletet. Ez legyőzhető egyedi párhuzamos kollektorok használatával.

Ha a folyamokat párhuzamos módban használja, kerülje a műveletek blokkolását, és használja a párhuzamos módot, ha a feladatok végrehajtásához hasonló időre van szükség (ha az egyik feladat sokkal hosszabb ideig tart, mint a másik, ez lelassíthatja az alkalmazás teljes munkafolyamatát).

A párhuzamos üzemmódban lévő adatfolyam a egymás utáni() módszer:

IntStream intStreamSequential = intStreamParallel.sequential (); logikai isParallel = intStreamSequential.isParallel ();

Következtetések

A Stream API hatékony, de egyszerűen érthető eszközkészlet az elemek sorozatának feldolgozásához. Ez lehetővé teszi számunkra, hogy csökkentsük a kazán kód hatalmas mennyiségét, olvashatóbb programokat hozzunk létre, és megfelelő alkalmazás esetén javítsuk az alkalmazás termelékenységét.

Az ebben a cikkben bemutatott kódminták többségében a folyamok el nem használtak (nem alkalmaztuk a Bezárás() módszer vagy terminál művelet). Egy igazi alkalmazásban ne hagyjon felhasználatlanul egy példányos adatfolyamot, mert ez memóriaszivárgáshoz vezet.

A cikkhez tartozó teljes kódminták a GitHub oldalon érhetők el.