Vodič za API Java 8 Stream
1. Pregled
U ovom detaljnom vodiču proći ćemo kroz praktičnu upotrebu Java 8 Streamova od stvaranja do paralelnog izvođenja.
Da bi razumjeli ovaj materijal, čitatelji moraju imati osnovno znanje o Java 8 (lambda izrazi, Neobvezno, reference metoda) i Stream API-ja. Ako niste upoznati s ovim temama, pogledajte naše prethodne članke - Nove značajke u Javi 8 i Uvod u Java 8 Streamove.
2. Stvaranje toka
Postoji mnogo načina za stvaranje instance streama različitih izvora. Jednom stvorena instanca neće mijenjati svoj izvor, dakle dopuštajući stvaranje više instanci iz jednog izvora.
2.1. Prazan tok
The prazan() metoda treba koristiti u slučaju stvaranja praznog toka:
Stream streamEmpty = Stream.empty ();
Čest je slučaj da prazan() metoda se koristi pri stvaranju kako bi se izbjegao povratak null za streamove bez elementa:
javni Stream streamOf (Popis popisa) return list == null
2.2. Potok od Kolekcija
Potok se također može stvoriti bilo koje vrste Kolekcija (Zbirka, popis, set):
Zbirka zbirke = Arrays.asList ("a", "b", "c"); Stream streamOfCollection = collection.stream ();
2.3. Tok niza
Niz također može biti izvor streama:
Stream streamOfArray = Stream.of ("a", "b", "c");
Oni se također mogu stvoriti iz postojećeg niza ili dijela niza:
String [] arr = new String [] {"a", "b", "c"}; Stream streamOfArrayFull = Nizovi.stream (arr); Stream streamOfArrayPart = Arrays.stream (arr, 1, 3);
2.4. Stream.builder ()
Kada se koristi graditelj željeni tip treba dodatno navesti u desnom dijelu izjave, inače izgraditi() metoda stvorit će instancu Prijenos:
Stream streamBuilder = Stream.builder (). Add ("a"). Add ("b"). Add ("c"). Build ();
2.5. Stream.generate ()
The generirati() metoda prihvaća a Dobavljač za generiranje elemenata. Kako je rezultirajući tok beskonačan, programer bi trebao navesti željenu veličinu ili generirati() metoda će raditi dok ne dosegne ograničenje memorije:
Stream streamGenerated = Stream.generate (() -> "element"). Limit (10);
Gornji kod stvara niz od deset nizova s vrijednošću - "element".
2.6. Stream.iterate ()
Drugi način stvaranja beskonačnog toka je pomoću ponoviti () metoda:
Stream streamIterated = Stream.iterate (40, n -> n + 2) .limit (20);
Prvi element rezultirajuće struje prvi je parametar ponoviti () metoda. Za stvaranje svakog sljedećeg elementa navedena se funkcija primjenjuje na prethodni element. U gornjem primjeru drugi element bit će 42.
2.7. Potok primitivaca
Java 8 nudi mogućnost stvaranja streamova od tri primitivna tipa: int, dugo i dvostruko. Kao Stream je generičko sučelje i ne postoji način da se primitivi koriste kao parametar tipa s generičkim, stvorena su tri nova posebna sučelja: IntStream, LongStream, DoubleStream.
Korištenje novih sučelja ublažava nepotrebno automatsko boksanje i omogućava veću produktivnost:
IntStream intStream = IntStream.range (1, 3); LongStream longStream = LongStream.rangeClosed (1, 3);
The raspon (int startInclusive, int endExclusive) metoda stvara uređeni tok od prvog parametra do drugog parametra. Povećava vrijednost sljedećih elemenata s korakom jednakim 1. Rezultat ne uključuje zadnji parametar, to je samo gornja granica niza.
The rangeClosed (int startInclusive, int endInclusive)metoda čini isto s samo jednom razlikom - uključen je drugi element. Ove dvije metode mogu se koristiti za generiranje bilo koje od tri vrste tokova primitiva.
Od Jave 8 Slučajno klasa pruža širok raspon metoda za generiranje tokova primitiva. Na primjer, sljedeći kod stvara a DoubleStream, koji ima tri elementa:
Slučajni slučajni = novi Random (); DoubleStream doubleStream = random.doubles (3);
2.8. Potok od Niz
Niz također se može koristiti kao izvor za stvaranje streama.
Uz pomoć znakovi () metoda Niz razred. Budući da nema sučelja CharStream u JDK, IntStream umjesto toga koristi se za predstavljanje struje znakova.
IntStream streamOfChars = "abc" .chars ();
Sljedeći primjer lomi a Niz u podnizove prema navedenom RegEx:
Stream streamOfString = Pattern.compile (",") .splitAsStream ("a, b, c");
2.9. Tok datoteke
Java NIO klasa Datoteke omogućuje generiranje a Stream tekstualne datoteke kroz linije () metoda. Svaki redak teksta postaje element toka:
Put puta = Paths.get ("C: \ file.txt"); Stream streamOfStrings = Files.lines (put); Stream streamWithCharset = Files.lines (put, Charset.forName ("UTF-8"));
The Charset može se navesti kao argument linije () metoda.
3. Upućivanje na tok
Moguće je instancirati tok i imati dostupnu referencu na njega sve dok su bile pozvane samo posredne operacije. Izvršavanje operacije terminala čini tok nepristupačnim.
Da bismo to demonstrirali, na neko ćemo vrijeme zaboraviti da je najbolja praksa uspostavljanje lančanog slijeda rada. Uz nepotrebnu opširnost, tehnički vrijedi sljedeći kod:
Stream stream = Stream.of ("a", "b", "c"). Filter (element -> element.contens ("b")); Po izboru anyElement = stream.findAny ();
Ali pokušaj ponovne upotrebe iste reference nakon pozivanja operacije terminala aktivirat će IllegalStateException:
Neobvezno firstElement = stream.findFirst ();
Kao IllegalStateException je RuntimeException, kompajler neće signalizirati problem. Dakle, vrlo je važno to upamtiti Java 8 potoci se ne mogu ponovno koristiti.
Ovakva vrsta ponašanja je logična jer su tokovi dizajnirani da pružaju mogućnost primjene konačnog niza operacija na izvor elemenata u funkcionalnom stilu, ali ne i za pohranu elemenata.
Dakle, da bi prethodni kod ispravno radio, treba napraviti neke promjene:
Elementi popisa = Stream.of ("a", "b", "c"). Filter (element -> element.contains ("b")) .collect (Collectors.toList ()); Izborno anyElement = elements.stream (). FindAny (); Neobvezno firstElement = elements.stream (). FindFirst ();
4. Cjevovod za strujanje
Za izvođenje niza operacija nad elementima izvora podataka i agregiranje njihovih rezultata potrebna su tri dijela - izvor, srednje operacije i a rad terminala.
Intermedijarne operacije vraćaju novi modificirani tok. Na primjer, za stvaranje novog toka postojećeg bez malo elemenata preskočiti() treba koristiti metodu:
Stream OnceModifiedStream = Stream.of ("abcd", "bbcd", "cbcd"). Preskoči (1);
Ako je potrebno više od jedne preinake, posredne operacije mogu biti ulančane. Pretpostavimo da također trebamo zamijeniti svaki element struje Stream s podnizom od prvih nekoliko znakova. To će se učiniti ulančavanjem preskočiti() i karta() metode:
Struji dvaputModifiedStream = stream.skip (1) .map (element -> element.substring (0, 3));
Kao što vidite, karta() metoda uzima parametar lambda izraz. Ako želite saznati više o lambdama, pogledajte naš vodič Lambda izrazi i funkcionalna sučelja: Savjeti i najbolji primjeri iz prakse.
Tok sam po sebi je bezvrijedan, stvarna stvar koju korisnika zanima rezultat je operacije terminala, koja može biti vrijednost neke vrste ili radnja primijenjena na svaki element toka. Po toku se može koristiti samo jedna operacija terminala.
Pravi i najprikladniji način korištenja streamova je a tok cjevovoda, koji je lanac izvora toka, posredne operacije i terminalni rad. Na primjer:
Popis popisa = Arrays.asList ("abc1", "abc2", "abc3"); duga veličina = list.stream (). preskoči (1) .map (element -> element.substring (0, 3)). sorted (). count ();
5. Lijeno prizivanje
Srednje operacije su lijene. Ovo znači to pozivat će se samo ako je to potrebno za izvršavanje operacije terminala.
Da biste to demonstrirali, zamislite da imamo metodu wasCalled (), koji povećava unutarnji brojač svaki put kad je pozvan:
privatni dugački brojač; privatna praznina wasCalled () {counter ++; }
Nazovimo metoduNazvan() iz rada filtar():
Popis popisa = Arrays.asList (“abc1”, “abc2”, “abc3”); brojač = 0; Stream stream = list.stream (). Filter (element -> {wasCalled (); return element.contains ("2");});
Kako imamo izvor od tri elementa, možemo pretpostaviti da je ta metoda filtar() pozvat će se tri puta i vrijednost brojač varijabla će biti 3. Ali pokretanje ovog koda se ne mijenja brojač uopće je još uvijek nula, dakle, filtar() metoda nije pozvana niti jednom. Razlog zašto - nedostaje rad terminala.
Prepišimo ovaj kod malo dodavanjem a karta() rad i terminal rad - findFirst (). Također ćemo dodati mogućnost praćenja redoslijeda poziva metoda uz pomoć evidentiranja:
Neobvezno stream = list.stream (). Filter (element -> {log.info ("pozvan je filter ()"); return element.contains ("2");}). Map (element -> {log.info ("map () je pozvan"); return element.toUpperCase ();}). findFirst ();
Rezultirajući zapisnik pokazuje da filtar() metoda je pozvana dva puta i karta() metoda samo jednom. To je tako jer se cjevovod izvodi vertikalno. U našem primjeru prvi element toka nije zadovoljio predikat filtra, a zatim filtar() metoda je pozvana za drugi element koji je proslijedio filtar. Bez pozivanja filtar() za treći element spustili smo se cjevovodom do karta() metoda.
The findFirst () operacija zadovoljava samo jedan element. Dakle, u ovom konkretnom primjeru lijeni poziv omogućio je izbjegavanje dva poziva metode - jedan za filtar() i jedan za karta().
6. Nalog izvršenja
S gledišta izvedbe, ispravan redoslijed jedan je od najvažnijih aspekata lančanih operacija u vodotoku:
long size = list.stream (). map (element -> {wasCalled (); return element.substring (0, 3);}). skip (2) .count ();
Izvršenje ovog koda povećat će vrijednost brojača za tri. To znači da karta() metoda potoka pozvana je tri puta. Ali vrijednost veličina je jedan. Dakle, rezultirajući tok ima samo jedan element i izvršili smo skupo karta() operacije bez razloga dva puta od tri puta.
Ako promijenimo redoslijed preskočiti() i karta() metode, the brojač povećat će se samo za jedan. Dakle, metoda karta() pozvat će se samo jednom:
duga veličina = list.stream (). skip (2) .map (element -> {wasCalled (); return element.substring (0, 3);}). count ();
Ovo nas dovodi do pravila: prije operacija koje se primjenjuju na svaki element trebaju se postaviti posredne operacije koje smanjuju veličinu struje. Dakle, zadržite metode poput skip (), filter (), različit () na vrhu vašeg vodotoka.
7. Smanjivanje struje
API ima mnogo terminalnih operacija koje agregiraju tok na tip ili na primitiv, na primjer, count (), max (), min (), sum (), ali ove operacije rade prema unaprijed definiranoj implementaciji. I što ako programer treba prilagoditi mehanizam smanjenja streama? Postoje dvije metode koje to omogućuju - smanjiti()i prikupiti() metode.
7.1. The smanjiti() Metoda
Postoje tri varijacije ove metode koje se razlikuju po svojim potpisima i vrstama vraćanja. Mogu imati sljedeće parametre:
identitet - početna vrijednost akumulatora ili zadana vrijednost ako je tok prazan i nema se što akumulirati;
akumulator - funkcija koja specificira logiku agregiranja elemenata. Kako akumulator stvara novu vrijednost za svaki korak smanjenja, količina novih vrijednosti jednaka je veličini toka i korisna je samo zadnja vrijednost. Ovo nije baš dobro za izvedbu.
kombinirač - funkcija koja agregira rezultate akumulatora. Kombinator se poziva samo u paralelnom načinu kako bi se smanjili rezultati akumulatora iz različitih niti.
Pa, pogledajmo ove tri metode na djelu:
OptionalInt smanjen = IntStream.range (1, 4) .reduce ((a, b) -> a + b);
smanjena = 6 (1 + 2 + 3)
int smanjioTwoParams = IntStream.range (1, 4) .reduce (10, (a, b) -> a + b);
smanjenaDvaParama = 16 (10 + 1 + 2 + 3)
int smanjenParams = Stream.of (1, 2, 3) .reduce (10, (a, b) -> a + b, (a, b) -> {log.info ("kombinator je pozvan"); vratite a + b;});
Rezultat će biti isti kao u prethodnom primjeru (16) i neće biti prijave što znači da kombinator nije pozvan. Da bi kombinirač radio, tok mora biti paralelan:
int reduceParallel = Arrays.asList (1, 2, 3) .parallelStream () .reduce (10, (a, b) -> a + b, (a, b) -> {log.info ("zvao se kombinirač" ); vrati a + b;});
Rezultat je ovdje drugačiji (36), a kombinacija je pozvana dva puta. Ovdje smanjenje djeluje prema sljedećem algoritmu: akumulator se pokrenuo tri puta dodavanjem svakog elementa toka u identitet na svaki element toka. Te se radnje rade paralelno. Kao rezultat, imaju (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;). Sada kombinirač može spojiti ova tri rezultata. Za to su mu potrebne dvije iteracije (12 + 13 = 25; 25 + 11 = 36).
7.2. The prikupiti() Metoda
Smanjenje struje može se izvršiti i drugom terminalnom operacijom - prikupiti() metoda. Prihvaća argument tipa Kolektor, koji precizira mehanizam smanjenja. Već postoje unaprijed definirani sakupljači za najčešće operacije. Pristupiti im je moguće uz pomoć Kolekcionari tip.
U ovom ćemo odjeljku koristiti sljedeće Popis kao izvor za sve tokove:
Popis productList = Arrays.asList (novi proizvod (23, "krumpir"), novi proizvod (14, "naranča"), novi proizvod (13, "limun"), novi proizvod (23, "kruh"), novi proizvod ( 13, "šećer"));
Pretvaranje toka u Kolekcija (Zbirka, Popis ili Postavi):
Popis collectorCollection = productList.stream (). Map (Product :: getName) .collect (Collectors.toList ());
Smanjujući na Niz:
String listToString = productList.stream (). Map (Product :: getName) .collect (Collectors.joining (",", "[", "]"));
The stolar() metoda može imati od jednog do tri parametra (graničnik, prefiks, sufiks). Najprikladnija stvar u korištenju stolar() - programer ne treba provjeriti doseže li tok tok kako bi primijenio sufiks, a ne primijenio graničnik. Kolektor pobrinut će se za to.
Obrada prosječne vrijednosti svih numeričkih elemenata toka:
dvostruka prosječna cijena = productList.stream () .collect (Collectors.averagingInt (Product :: getPrice));
Obrada zbroja svih numeričkih elemenata toka:
int summingPrice = productList.stream () .collect (Collectors.summingInt (Product :: getPrice));
Metode prosječnoXX (), zbrajanjeXX () i sažimanjeXX () može raditi kao s primitivima (int, dugo, dvostruko) kao kod klasa omota (Cijeli, dugi, dvostruki). Još jedna snažna značajka ovih metoda je pružanje mapiranja. Dakle, programer ne treba koristiti dodatni karta() operacija prije prikupiti() metoda.
Prikupljanje statističkih podataka o elementima toka:
Statistika IntSummaryStatistics = productList.stream () .collect (Collectors.summarizingInt (Product :: getPrice));
Korištenjem rezultirajuće instance tipa IntSummaryStatistics programer može stvoriti statističko izvješće primjenom toString () metoda. Rezultat će biti Niz zajedničko ovome “IntSummaryStatistics {broj = 5, zbroj = 86, min = 13, prosjek = 17,200000, maks = 23}”.
Također je lako iz ovog objekta izdvojiti zasebne vrijednosti za count, sum, min, prosjek primjenom metoda getCount (), getSum (), getMin (), getAverage (), getMax (). Sve ove vrijednosti mogu se izdvojiti iz jednog cjevovoda.
Grupiranje elemenata toka prema navedenoj funkciji:
Karta collectorMapOfLists = productList.stream () .collect (Collectors.groupingBy (Product :: getPrice));
U gornjem primjeru potok je smanjen na Karta koja sve proizvode grupira po cijeni.
Podjela elemenata struje u skupine prema nekom predikatu:
Karta mapPartionary = productList.stream () .collect (Collectors.partitioningBy (element -> element.getPrice ()> 15));
Guranje kolektora radi dodatne transformacije:
Postavite unmodifiableSet = productList.stream () .collect (Collectors.collectingAndThen (Collectors.toSet (), Collections :: unmodifiableSet));
U ovom konkretnom slučaju, kolektor je pretvorio tok u Postavi a zatim stvorio neizmijenjivo Postavi izvan nje.
Prikupljač po mjeri:
Ako bi se iz nekog razloga trebao izraditi prilagođeni sakupljač, najlakši i manje opširan način to je korištenje metode od() tipa Kolektor.
Kolektor toLinkedList = Collector.of (LinkedList :: new, LinkedList :: add, (first, second) -> {first.addAll (second); return first;}); LinkedList linkedListOfPersons = productList.stream (). Collect (toLinkedList);
U ovom primjeru, primjer datoteke Kolektor se sveo na LinkedList.
Paralelni tokovi
Prije Jave 8 paralelizacija je bila složena. Nastajanje ExecutorService i ForkJoin malo pojednostavnio život programera, ali ipak bi trebali imati na umu kako stvoriti određenog izvršitelja, kako ga pokrenuti i tako dalje.Java 8 uvela je način postizanja paralelizma u funkcionalnom stilu.
API omogućuje stvaranje paralelnih tokova koji izvode operacije u paralelnom načinu. Kad je izvor potoka a Kolekcija ili an niz to se može postići uz pomoć paralelni tok () metoda:
Stream streamOfCollection = productList.parallelStream (); boolean isParallel = streamOfCollection.isParallel (); boolean bigPrice = streamOfCollection .map (product -> product.getPrice () * 12) .anyMatch (cijena -> cijena> 200);
Ako je izvor toka nešto drugačije od a Kolekcija ili an niz, paralelno() treba koristiti metodu:
IntStream intStreamParallel = IntStream.range (1, 150) .parallel (); boolean isParallel = intStreamParallel.isParallel ();
Ispod haube, Stream API automatski koristi ForkJoin okvir za paralelno izvršavanje operacija. Prema zadanim postavkama koristit će se zajedničko spremište niti i ne može mu se (barem zasad) dodijeliti neko prilagođeno spremište niti. To se može prevladati korištenjem prilagođenog skupa paralelnih kolektora.
Kada upotrebljavate streamove u paralelnom načinu, izbjegavajte blokiranje operacija i upotrijebite paralelni način kada zadaci trebaju slično vrijeme za izvršavanje (ako jedan zadatak traje puno dulje od drugog, to može usporiti cijeli radni tok aplikacije).
Struja u paralelnom načinu može se pretvoriti natrag u sekvencijalni način pomoću sekvencijalno() metoda:
IntStream intStreamSequential = intStreamParallel.sequential (); logička isParallel = intStreamSequential.isParallel ();
Zaključci
Stream API moćan je, ali jednostavan za razumijevanje skup alata za obradu slijeda elemenata. Omogućuje nam smanjenje velike količine osnovnog koda, stvaranje čitljivijih programa i poboljšanje produktivnosti aplikacije kada se pravilno koristi.
U većini uzoraka koda prikazanih u ovom članku potoci su ostali nepotrošeni (nismo primijenili Zatvoriti() metoda ili operacija terminala). U pravoj aplikaciji, ne ostavljajte instancirane tokove nekonzumirane jer će to dovesti do curenja memorije.
Cjeloviti uzorci koda koji prate članak dostupni su na GitHubu.