Výukový program Java 8 Stream API

1. Prehľad

V tomto podrobnom výučbe sa oboznámime s praktickým využitím prúdov Java 8 od vytvorenia po paralelné vykonávanie.

Aby porozumeli tomuto materiálu, musia mať čitatelia základné znalosti jazyka Java 8 (výrazy lambda, Voliteľné, metódy) a Stream API. Ak nie ste oboznámení s týmito témami, pozrite si naše predchádzajúce články - Nové funkcie v prostredí Java 8 a Úvod do prúdov Java 8.

2. Tvorba streamu

Existuje mnoho spôsobov, ako vytvoriť inštanciu prúdu z rôznych zdrojov. Po vytvorení inštancie nebude meniť svoj zdroj, teda umožňuje vytvorenie viacerých inštancií z jedného zdroja.

2.1. Prázdny prúd

The prázdne () metóda by sa mala použiť v prípade vytvorenia prázdneho streamu:

Stream streamEmpty = Stream.empty ();

Často sa stáva, že prázdne () pri vytváraní sa používa metóda, aby sa zabránilo návratu nulový pre streamy bez prvku:

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

2.2. Prúd z Zbierka

Stream je možné vytvoriť aj z ľubovoľného typu Zbierka (Zbierka, zoznam, sada):

Zbierka zbierka = Arrays.asList ("a", "b", "c"); Stream streamOfCollection = collection.stream ();

2.3. Prúd poľa

Pole môže byť tiež zdrojom Stream:

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

Môžu byť tiež vytvorené z existujúceho poľa alebo z časti poľa:

Reťazec [] arr = nový Reťazec [] {"a", "b", "c"}; Stream streamOfArrayFull = Arrays.stream (arr); Stream streamOfArrayPart = Arrays.stream (arr, 1, 3);

2.4. Stream.builder ()

Keď sa použije staviteľ požadovaný typ by mal byť dodatočne uvedený v pravej časti výpisu, inak build () metóda vytvorí inštanciu súboru Prúd:

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

2.5. Stream.generate ()

The generovať () metóda akceptuje a Dodávateľ na generovanie prvkov. Pretože je výsledný prúd nekonečný, vývojár by mal určiť požadovanú veľkosť alebo generovať () metóda bude fungovať, kým nedosiahne limit pamäte:

Stream streamGenerated = Stream.generate (() -> "prvok"). Limit (10);

Vyššie uvedený kód vytvára postupnosť desiatich reťazcov s hodnotou - "element".

2.6. Stream.iterate ()

Ďalším spôsobom, ako vytvoriť nekonečný prúd, je použitie iterovať () metóda:

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

Prvý prvok výsledného toku je prvý parameter parametra iterovať () metóda. Na vytvorenie každého nasledujúceho prvku sa zadaná funkcia použije na predchádzajúci prvok. V príklade vyššie bude druhý prvok 42.

2.7. Prúd primitívov

Java 8 ponúka možnosť vytvárať streamy z troch primitívnych typov: int, dlho a dvojitý. Ako Prúd je všeobecné rozhranie a neexistuje nijaký spôsob použitia primitív ako parametra typu v prípade generík, boli vytvorené tri nové špeciálne rozhrania: IntStream, LongStream, DoubleStream.

Používanie nových rozhraní zmierňuje zbytočné automatické boxovanie a umožňuje zvýšenú produktivitu:

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

The rozsah (int startInclusive, int endExclusive) metóda vytvorí usporiadaný prúd z prvého parametra do druhého parametra. Zvyšuje hodnotu nasledujúcich prvkov s krokom rovným 1. Výsledok nezahŕňa posledný parameter, je to iba horná hranica sekvencie.

The rangeClosed (int startInclusive, int endInclusive)metóda robí to isté iba s jedným rozdielom - druhý prvok je zahrnutý. Tieto dve metódy možno použiť na generovanie ktoréhokoľvek z troch typov prúdov primitívov.

Od verzie Java 8 Náhodné trieda poskytuje širokú škálu metód na generovanie prúdov primitívov. Napríklad nasledujúci kód vytvorí a DoubleStream, ktorý má tri prvky:

Random random = nový Random (); DoubleStream doubleStream = random.doubles (3);

2.8. Prúd z String

String možno tiež použiť ako zdroj na vytvorenie streamu.

S pomocou znaky () metóda String trieda. Pretože neexistuje rozhranie CharStream v JDK IntStream sa používa na predstavenie prúdu znakov.

IntStream streamOfChars = "abc" .chars ();

Nasledujúci príklad zlomí a String do podreťazcov podľa zadaného RegEx:

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

2.9. Stream súboru

Trieda Java NIO Súbory umožňuje generovať a Prúd textového súboru prostredníctvom riadky () metóda. Každý riadok textu sa stáva prvkom streamu:

Path path = Paths.get ("C: \ file.txt"); Stream streamOfStrings = Files.lines (cesta); Stream streamWithCharset = Files.lines (cesta, Charset.forName ("UTF-8"));

The Charset možno určiť ako argument riadky () metóda.

3. Odkazy na stream

Je možné vytvoriť inštanciu prúdu a mať k nemu prístupný odkaz, pokiaľ sa volali iba medzioperácie. Vykonanie operácie terminálu spôsobí, že stream je neprístupný.

Aby sme to demonštrovali, na chvíľu zabudneme, že najlepším postupom je reťazenie postupnosti operácií. Okrem zbytočnej výrečnosti je technicky platný aj tento kód:

Stream stream = Stream.of ("a", "b", "c"). Filter (element -> element.contains ("b")); Voliteľné anyElement = stream.findAny ();

Pokus o opätovné použitie tej istej referencie po vyvolaní operácie terminálu však spustí IllegalStateException:

Voliteľné firstElement = stream.findFirst ();

Ako IllegalStateException je a RuntimeException, kompilátor nebude signalizovať problém. Je preto veľmi dôležité pamätať na to Java 8 streamy sa nedajú znova použiť.

Tento druh správania je logický, pretože toky boli navrhnuté tak, aby poskytovali schopnosť aplikovať konečnú postupnosť operácií na zdroj prvkov vo funkčnom štýle, ale nie na ich ukladanie.

Aby správne fungoval predchádzajúci kód, je potrebné vykonať určité zmeny:

Zoznam prvkov = Stream.of ("a", "b", "c"). Filter (element -> element.contains ("b")) .collect (Collectors.toList ()); Voliteľné anyElement = elements.stream (). FindAny (); Voliteľné firstElement = elements.stream (). FindFirst ();

4. Potrubie toku

Na vykonanie postupnosti operácií nad prvkami zdroja údajov a na agregáciu ich výsledkov sú potrebné tri časti - zdroj, medzioperácie a a terminálna prevádzka.

Sprostredkovateľské operácie vrátia nový upravený prúd. Napríklad na vytvorenie nového toku existujúceho bez niekoľkých prvkov preskočiť () mala by sa použiť metóda:

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

Ak je potrebných viac ako jedna úprava, môžu byť sprostredkovateľské operácie zreťazené. Predpokladajme, že musíme tiež nahradiť každý prvok prúdu Prúd s podreťazcom prvých pár znakov. To sa uskutoční reťazením reťazca preskočiť () a mapa () metódy:

Stream dvakrátModifiedStream = stream.skip (1) .map (element -> element.substring (0, 3));

Ako vidíte, mapa () metóda berie ako parameter výraz lambda. Ak sa chcete dozvedieť viac informácií o lambdách, pozrite si náš výukový program Lambda výrazy a funkčné rozhrania: tipy a osvedčené postupy.

Samotný stream je bezcenný, skutočná vec, o ktorú sa užívateľ zaujíma, je výsledok činnosti terminálu, čo môže byť hodnota nejakého typu alebo akcia aplikovaná na každý prvok streamu. Na jeden stream je možné použiť iba jednu operáciu terminálu.

Správny a najpohodlnejší spôsob použitia streamov je a stream pipeline, čo je reťazec zdroja prúdu, sprostredkujúce operácie a terminálna prevádzka. Napríklad:

Zoznam list = Arrays.asList ("abc1", "abc2", "abc3"); long size = list.stream (). skip (1) .map (element -> element.substring (0, 3)). ordered (). count ();

5. Lenivá invokácia

Medzioperácie sú lenivé. To znamená, že budú vyvolané, iba ak je to potrebné na vykonanie operácie terminálu.

Aby sme to demonštrovali, predstavte si, že máme metódu bol volaný(), ktorý zvyšuje vnútorné počítadlo zakaždým, keď bolo volané:

súkromný dlhý pult; private void wasCalled () {counter ++; }

Zavolajme metóda bolaVolal() z prevádzky filter ():

Zoznam list = Arrays.asList (“abc1”, “abc2”, “abc3”); pult = 0; Stream stream = list.stream (). Filter (element -> {wasCalled (); návratový element.contains ("2");});

Pretože máme zdroj troch prvkov, môžeme túto metódu predpokladať filter () sa zavolá trikrát a hodnota pult premenná bude 3. Spustenie tohto kódu sa však nezmení pult vôbec, stále je to nula, takže filter () metóda nebola volaná ani raz. Dôvod prečo - chýba prevádzka terminálu.

Poďme tento kód trochu prepísať pridaním a mapa () prevádzka a prevádzka terminálu - findFirst (). Pridáme tiež možnosť sledovať poradie volaní metód pomocou protokolovania:

Voliteľný stream = list.stream (). Filter (element -> {log.info ("filter () sa volal"); návrat element.contains ("2");}). Mapa (element -> {log.info ("mapa () bola volaná"); návratový prvok.toUpperCase ();}). findFirst ();

Výsledný protokol ukazuje, že filter () metóda bola volaná dvakrát a mapa () metóda iba raz. Je to tak preto, lebo potrubie sa vykonáva vertikálne. V našom príklade prvý prvok streamu nevyhovoval predikátu filtra, potom filter () bola vyvolaná metóda pre druhý prvok, ktorý prešiel filtrom. Bez volania filter () pre tretí prvok sme šli dolu potrubím k mapa () metóda.

The findFirst () prevádzka uspokojuje iba jeden prvok. V tomto konkrétnom príklade teda lenivé vyvolanie umožnilo vyhnúť sa dvom volaním metód - jednému pre filter () a jeden pre mapa ().

6. Exekučný poriadok

Z hľadiska výkonu správne poradie je jedným z najdôležitejších aspektov reťazových operácií v toku plynovodov:

long size = list.stream (). map (element -> {wasCalled (); return element.substring (0, 3);}). skip (2) .count ();

Vykonanie tohto kódu zvýši hodnotu počítadla o tri. To znamená, že mapa () metóda toku bola volaná trikrát. Ale hodnota veľkosť je jeden. Výsledný prúd má teda iba jeden prvok a vykonali sme drahý mapa () operácie bezdôvodne dvakrát z troch.

Ak zmeníme poradie preskočiť () a mapa () metódy, the pult sa zvýši iba o jeden. Takže metóda mapa () bude zavolaný iba raz:

long size = list.stream (). skip (2) .map (element -> {wasCalled (); return element.substring (0, 3);}). count ();

Týmto sa dostávame k pravidlu: prechodné operácie, ktoré znižujú veľkosť toku, by sa mali umiestniť pred operácie, ktoré sa vzťahujú na každý prvok. Takže si ponechajte také metódy ako skip (), filter (), zreteľný () v hornej časti kanála streamu.

7. Redukcia toku

API má veľa terminálových operácií, ktoré agregujú prúd na typ alebo na primitívny, napríklad count (), max (), min (), sum (), ale tieto operácie fungujú podľa preddefinovanej implementácie. A čo ak vývojár potrebuje prispôsobiť redukčný mechanizmus streamu? Existujú dve metódy, ktoré to umožňujú - znížiť ()a zbierať () metódy.

7.1. The znížiť () Metóda

Existujú tri variácie tejto metódy, ktoré sa líšia svojimi podpismi a návratovými typmi. Môžu mať nasledujúce parametre:

identita - počiatočná hodnota pre akumulátor alebo predvolená hodnota, ak je prúd prázdny a nie je čo akumulovať;

akumulátor - funkcia, ktorá špecifikuje logiku agregácie prvkov. Pretože akumulátor vytvára novú hodnotu pre každý krok znižovania, množstvo nových hodnôt sa rovná veľkosti toku a užitočná je iba posledná hodnota. To nie je veľmi dobré pre výkon.

kombinátor - funkcia, ktorá agreguje výsledky akumulátora. Kombinátor sa volá iba v paralelnom režime, aby sa znížili výsledky akumulátorov z rôznych vlákien.

Pozrime sa teda na tieto tri metódy v akcii:

OptionalInt reduced = IntStream.range (1, 4) .reduce ((a, b) -> a + b);

znížený = 6 (1 + 2 + 3)

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

zníženéDvaParammy = 16 (10 + 1 + 2 + 3)

int reducedParams = Stream.of (1, 2, 3) .reduce (10, (a, b) -> a + b, (a, b) -> {log.info ("volal sa kombinátor"); vrátiť a + b;});

Výsledok bude rovnaký ako v predchádzajúcom príklade (16) a nebude sa prihlasovať, čo znamená, že kombinátor nebol zavolaný. Aby kombinátor fungoval, prúd by mal byť paralelný:

int reducedParallel = Arrays.asList (1, 2, 3) .parallelStream () .reduce (10, (a, b) -> a + b, (a, b) -> {log.info ("volal sa kombinátor") ); vrátiť a + b;});

Výsledok je tu iný (36) a kombinátor bol zavolaný dvakrát. Tu redukcia funguje podľa nasledujúceho algoritmu: akumulátor bežal trikrát pridaním všetkých prvkov streamu identita ku každému prvku streamu. Tieto akcie sa uskutočňujú súbežne. Vo výsledku majú (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;). Teraz môže kombinátor spojiť tieto tri výsledky. Potrebuje na to dve iterácie (12 + 13 = 25; 25 + 11 = 36).

7.2. The zbierať () Metóda

Redukciu toku je možné vykonať aj inou operáciou terminálu - zbierať () metóda. Prijíma argument typu Zberateľ, ktorá špecifikuje mechanizmus redukcie. Pre väčšinu bežných operácií sú už vytvorené preddefinované kolektory. Je k nim prístup pomocou Zberatelia typu.

V tejto časti použijeme nasledujúce Zoznam ako zdroj pre všetky streamy:

Zoznam productList = Arrays.asList (nový produkt (23, „zemiaky“), nový produkt (14, „oranžový“), nový produkt (13, „citrón“), nový produkt (23, „chlieb“), nový produkt ( 13, „cukor“));

Prebieha konverzia streamu na Zbierka (Zbierka, Zoznam alebo Nastaviť):

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

Zníženie na String:

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

The stolár () metóda môže mať jeden až tri parametre (oddeľovač, predpona, prípona). To najpríjemnejšie na používaní stolár () - vývojár nemusí kontrolovať, či stream dosiahol koniec, aby mohol použiť príponu a nie oddeľovač. Zberateľ postará sa o to.

Spracovanie priemernej hodnoty všetkých číselných prvkov streamu:

dvojnásobok priemernej ceny = productList.stream () .collect (Collectors.averagingInt (Product :: getPrice));

Spracovanie súčtu všetkých číselných prvkov streamu:

int summingPrice = productList.stream () .collect (Collectors.summingInt (Product :: getPrice));

Metódy averagingXX (), summingXX () a summarizingXX () môže pracovať ako s primitívmi (int, long, double) ako u svojich tried obalov (Celé číslo, dlhé, dvojité). Jednou z výkonnejších funkcií týchto metód je poskytovanie mapovania. Vývojár teda nemusí používať ďalšie mapa () operácia pred zbierať () metóda.

Zhromažďovanie štatistických informácií o prvkoch streamu:

Štatistika IntSummaryStatistics = productList.stream () .collect (Collectors.summarizingInt (Product :: getPrice));

Použitím výslednej inštancie typu IntSummaryStatistics vývojár môže vytvoriť štatistickú správu použitím natiahnuť() metóda. Výsledkom bude a String spoločné s týmto „IntSummaryStatistics {count = 5, sum = 86, min = 13, priemer = 17 200000, max = 23}“.

Z tohto objektu je tiež ľahké extrahovať samostatné hodnoty pre počet, súčet, min., priemer uplatňovaním metód getCount (), getSum (), getMin (), getAverage (), getMax (). Všetky tieto hodnoty je možné extrahovať z jedného potrubia.

Zoskupenie prvkov streamu podľa zadanej funkcie:

Mapa collectorMapOfLists = productList.stream () .collect (Collectors.groupingBy (Product :: getPrice));

V príklade vyššie bol prúd znížený na Mapa ktorá zoskupuje všetky výrobky podľa ich ceny.

Rozdelenie prvkov streamu do skupín podľa určitého predikátu:

Mapa mapPartlined = productList.stream () .collect (Collectors.partitioningBy (element -> element.getPrice ()> 15));

Stlačenie kolektora na vykonanie ďalšej transformácie:

Set unmodifiableSet = productList.stream () .collect (Collectors.collectingAndThen (Collectors.toSet (), Collections :: unmodifiableSet));

V tomto konkrétnom prípade kolektor premenil prúd na a Nastaviť a potom vytvoril neupraviteľné Nastaviť von z toho.

Vlastný zberateľ:

Ak by sa z nejakého dôvodu mal vytvoriť vlastný kolektor, najjednoduchší a menej verný spôsob je použitie metódy z () typu Zberateľ.

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

V tomto príklade je inštancia súboru Zberateľ sa znížil na LinkedList.

Paralelné toky

Pred jazykom Java 8 bola paralelizácia zložitá. Rozvíjajúce sa z ExecutorService a ForkJoin zjednodušený život vývojárov, mali by si však uvedomiť, ako vytvoriť konkrétneho exekútora, ako ho spustiť a podobne.Java 8 predstavila spôsob dosiahnutia paralelizmu vo funkčnom štýle.

API umožňuje vytváranie paralelných streamov, ktoré vykonávajú operácie v paralelnom režime. Keď je zdrojom streamu a Zbierka alebo an pole dá sa to dosiahnuť pomocou parallelStream () metóda:

Stream streamOfCollection = productList.parallelStream (); boolean isParallel = streamOfCollection.isParallel (); boolean bigPrice = streamOfCollection .map (product -> product.getPrice () * 12) .anyMatch (cena -> cena> 200);

Ak je zdrojom streamu niečo iné ako a Zbierka alebo an pole, paralelné () mala by sa použiť metóda:

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

Stream API pod kapotou automaticky používa ForkJoin rámec na paralelné vykonávanie operácií. Predvolene sa použije spoločný fond vlákien a neexistuje spôsob (aspoň zatiaľ), ako mu priradiť nejaký vlastný fond vlákien. To sa dá prekonať použitím vlastnej sady paralelných kolektorov.

Pri používaní streamov v paralelnom režime sa vyvarujte blokovania operácií a paralelný režim používajte, keď úlohy potrebujú podobné množstvo času na vykonanie (ak jedna úloha trvá oveľa dlhšie ako druhá, môže to spomaliť celý pracovný tok aplikácie).

Prúd v paralelnom režime možno prevádzať späť do sekvenčného režimu pomocou postupné () metóda:

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

Závery

Stream API je výkonná, ale ľahko pochopiteľná sada nástrojov na spracovanie postupnosti prvkov. Umožňuje nám to znížiť obrovské množstvo štandardných kódov, vytvoriť čitateľnejšie programy a zvýšiť produktivitu aplikácie pri správnom použití.

Vo väčšine vzorov kódu uvedených v tomto článku zostali prúdy nespotrebované (neaplikovali sme Zavrieť() metóda alebo terminálna prevádzka). V skutočnej aplikácii nenechávajte instantizované prúdy nespotrebované, pretože to povedie k úniku pamäte.

Kompletné ukážky kódu, ktoré sú priložené k článku, sú k dispozícii na GitHub.


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