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.