Java 8 Stream API-opplæringen

1. Oversikt

I denne grundige opplæringen vil vi gå gjennom den praktiske bruken av Java 8 Streams fra opprettelse til parallell kjøring.

For å forstå dette materialet, må leserne ha grunnleggende kunnskap om Java 8 (lambda-uttrykk, Valgfri, metodereferanser) og av Stream API. Hvis du ikke er kjent med disse emnene, kan du ta en titt på våre tidligere artikler - Nye funksjoner i Java 8 og Introduksjon til Java 8 Streams.

2. Stream Creation

Det er mange måter å lage en strømforekomst av forskjellige kilder. Når den er opprettet, forekomsten vil ikke endre kilden, derfor tillater oppretting av flere forekomster fra en enkelt kilde.

2.1. Tom strøm

De tømme() metoden skal brukes i tilfelle oppretting av en tom strøm:

Stream streamEmpty = Stream.empty ();

Det er ofte slik at tømme() metoden brukes ved opprettelsen for å unngå retur null for bekker uten element:

public Stream streamOf (List list) returliste == null 

2.2. Strøm av Samling

Stream kan også opprettes av alle typer Samling (Samling, liste, sett):

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

2.3. Stream of Array

Array kan også være en kilde til en Stream:

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

De kan også opprettes av en eksisterende matrise eller av en del av en matrise:

String [] arr = new String [] {"a", "b", "c"}; Stream streamOfArrayFull = Arrays.stream (arr); Stream streamOfArrayPart = Arrays.stream (arr, 1, 3);

2.4. Stream.builder ()

Når byggmester brukes ønsket type skal i tillegg være spesifisert i høyre del av uttalelsen, ellers bygge() metoden vil opprette en forekomst av Strøm:

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

2.5. Stream.generate ()

De generere() metoden godtar en Leverandør for elementgenerering. Siden den resulterende strømmen er uendelig, bør utvikleren angi ønsket størrelse eller generere() metoden vil fungere til den når minnegrensen:

Stream streamGenerated = Stream.generate (() -> "element"). Grense (10);

Koden ovenfor lager en sekvens på ti strenger med verdien - "element".

2.6. Stream.iterate ()

En annen måte å skape en uendelig strøm på er å bruke repetere() metode:

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

Det første elementet i den resulterende strømmen er en første parameter for repetere() metode. For å lage hvert følgende element blir den spesifiserte funksjonen brukt på det forrige elementet. I eksemplet over vil det andre elementet være 42.

2.7. Strøm av primitiver

Java 8 gir muligheten til å lage strømmer av tre primitive typer: int, lang og dobbelt. Som Strøm er et generisk grensesnitt og det er ingen måte å bruke primitive som en typeparameter med generiske, tre nye spesialgrensesnitt ble opprettet: IntStream, LongStream, DoubleStream.

Ved å bruke de nye grensesnittene lindres unødvendig automatisk boksing, gir økt produktivitet

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

De rekkevidde (int startInclusive, int endExclusive) metoden oppretter en ordnet strøm fra den første parameteren til den andre parameteren. Den øker verdien av påfølgende elementer med trinnet lik 1. Resultatet inkluderer ikke den siste parameteren, det er bare en øvre grense for sekvensen.

De rangeClosed (int startInclusive, int endInclusive)metoden gjør det samme med bare en forskjell - det andre elementet er inkludert. Disse to metodene kan brukes til å generere hvilken som helst av de tre typer primitivstrømmer.

Siden Java 8 den Tilfeldig klasse gir et bredt spekter av metoder for generering av primitiver. For eksempel oppretter følgende kode en DoubleStream, som har tre elementer:

Tilfeldig tilfeldig = ny Tilfeldig (); DoubleStream doubleStream = tilfeldig. Dobler (3);

2.8. Strøm av String

String kan også brukes som kilde for å lage en strøm.

Ved hjelp av tegn () metoden for String klasse. Siden det ikke er noe grensesnitt CharStream i JDK, the IntStream brukes til å representere en strøm av tegn i stedet.

IntStream streamOfChars = "abc" .chars ();

Følgende eksempel bryter a String i understrenger i henhold til spesifisert RegEx:

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

2.9. Strøm av fil

Java NIO-klasse Filer tillater å generere en Strøm av en tekstfil gjennom linjer () metode. Hver linje i teksten blir et element i strømmen:

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

De Charset kan spesifiseres som et argument for linjer () metode.

3. Henvisning til en strøm

Det er mulig å sette i gang en strøm og ha en tilgjengelig referanse til den så lenge bare mellomliggende operasjoner ble kalt. Å utføre en terminaloperasjon gjør en strøm utilgjengelig.

For å demonstrere dette vil vi glemme en stund at den beste fremgangsmåten er å kjede sekvens av operasjon. Foruten den unødvendige ordlighetsgraden, er følgende kode teknisk gyldig:

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

Men et forsøk på å bruke den samme referansen etter å ha ringt terminaloperasjonen vil utløse IllegalStateException:

Valgfritt firstElement = stream.findFirst ();

Som den IllegalStateException er en RuntimeException, vil en kompilator ikke signalisere om et problem. Så det er veldig viktig å huske det Java 8 bekker kan ikke brukes på nytt.

Denne typen oppførsel er logisk fordi strømmer ble designet for å gi en evne til å bruke en endelig sekvens av operasjoner til kilden til elementer i en funksjonell stil, men ikke for å lagre elementer.

Så for å få tidligere kode til å fungere ordentlig, bør det gjøres noen endringer:

Listeelementer = Stream.of ("a", "b", "c"). Filter (element -> element.contains ("b")) .collect (Collectors.toList ()); Valgfritt anyElement = elements.stream (). FindAny (); Valgfritt firstElement = elements.stream (). FindFirst ();

4. Strømrørledning

For å utføre en sekvens av operasjoner over elementene i datakilden og samle resultatene, trengs det tre deler - kilde, mellomdrift (er) og en terminaldrift.

Mellomliggende operasjoner returnerer en ny modifisert strøm. For eksempel, for å lage en ny strøm av den eksisterende uten få elementer hopp over () metoden skal brukes:

Strøm onceModifiedStream = Stream.of ("abcd", "bbcd", "cbcd"). Hopp over (1);

Hvis mer enn én modifikasjon er nødvendig, kan mellomoperasjoner lenkes. Anta at vi også må erstatte hvert element av strøm Strøm med en understreng av de første få tegnene. Dette vil bli gjort ved å lenke hopp over () og kart() metoder:

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

Som du kan se, er kart() metoden tar et lambdauttrykk som parameter. Hvis du vil lære mer om lambdas, kan du se på veiledningen Lambda Expressions and Functional Interfaces: Tips and Best Practices.

En strøm i seg selv er verdiløs, den virkelige tingen en bruker er interessert i er et resultat av terminaloperasjonen, som kan være en verdi av en hvilken som helst type eller en handling som brukes på hvert element i strømmen. Bare én terminaloperasjon kan brukes per strøm.

Den rette og mest praktiske måten å bruke strømmer på er strømrørledning, som er en kjede av strømkilde, mellomliggende operasjoner og en terminaloperasjon. For eksempel:

Listeliste = Arrays.asList ("abc1", "abc2", "abc3"); lang størrelse = liste.strøm (). hopp over (1). kart (element -> element.substring (0, 3)). sortert (). count ();

5. Lat påkallelse

Mellomliggende operasjoner er lat. Dette betyr at de vil bare bli påkalt hvis det er nødvendig for utførelse av terminaloperasjonen.

For å demonstrere dette, forestill deg at vi har metode ble kalt(), som øker en indre teller hver gang den ble kalt:

privat lang skranke; private void wasCalled () {counter ++; }

La oss ringe metoden varKalt() fra drift filter():

Listeliste = Arrays.asList (“abc1”, “abc2”, “abc3”); teller = 0; Stream stream = list.stream (). Filter (element -> {wasCalled (); return element.contains ("2");});

Siden vi har en kilde til tre elementer, kan vi anta den metoden filter() vil bli kalt tre ganger og verdien av disk variabel vil være 3. Men å kjøre denne koden endres ikke disk i det hele tatt er det fortsatt null, så, den filter() metoden ble ikke kalt en gang. Årsaken til - mangler terminaloperasjonen.

La oss omskrive denne koden litt ved å legge til en kart() drift og terminaloperasjon - findFirst (). Vi vil også legge til en evne til å spore en rekkefølge av metodeanrop ved hjelp av logging:

Valgfri stream = list.stream (). Filter (element -> {log.info ("filter () ble kalt"); retur element.contains ("2");}). Kart (element -> {log.info ("map () ble kalt"); return element.toUpperCase ();}). findFirst ();

Resulterende logg viser at filter() metoden ble kalt to ganger og kart() metoden bare en gang. Det er slik fordi rørledningen kjøres vertikalt. I vårt eksempel tilfredsstilte ikke det første elementet i strømmen filterets predikat, deretter filter() metoden ble påkalt for det andre elementet, som passerte filteret. Uten å ringe filter() for tredje element gikk vi ned gjennom rørledningen til kart() metode.

De findFirst () operasjonen tilfredsstiller bare ett element. Så, i dette spesielle eksemplet tillot lat påkallelse å unngå to metodesamtaler - en for filter() og en for kart().

6. Orden for utførelse

Fra ytelsessynspunktet, riktig rekkefølge er en av de viktigste aspektene ved kjededrift i strømrørledningen:

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

Utførelse av denne koden vil øke verdien på telleren med tre. Dette betyr at kart() metoden for strømmen ble kalt tre ganger. Men verdien av størrelse er en. Så den resulterende strømmen har bare ett element, og vi utførte det dyre kart() operasjoner uten grunn to ganger av tre ganger.

Hvis vi endrer rekkefølgen på hopp over () og kart() metoder, de disk vil bare øke med en. Så, metoden kart() vil bli kalt bare en gang:

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

Dette bringer oss opp til regelen: mellomoperasjoner som reduserer størrelsen på strømmen, bør plasseres før operasjoner som gjelder for hvert element. Så hold slike metoder som skip (), filter (), distinkt () på toppen av strømrørledningen din.

7. Strømreduksjon

API har mange terminaloperasjoner som samler en strøm til en type eller til en primitiv, for eksempel count (), max (), min (), sum (), men disse operasjonene fungerer i henhold til den forhåndsdefinerte implementeringen. Og hva hvis en utvikler trenger å tilpasse en Streams reduksjonsmekanisme? Det er to metoder som gjør det mulig å gjøre dette - redusere()og samle inn() metoder.

7.1. De redusere() Metode

Det er tre varianter av denne metoden, som skiller seg ut fra signaturer og returtyper. De kan ha følgende parametere:

identitet - den opprinnelige verdien for en akkumulator eller en standardverdi hvis en strøm er tom og det ikke er noe å akkumulere;

akkumulator - en funksjon som spesifiserer en logikk for aggregering av elementer. Når akkumulatoren skaper en ny verdi for hvert trinn med å redusere, tilsvarer mengden nye verdier strømens størrelse, og bare den siste verdien er nyttig. Dette er ikke veldig bra for forestillingen.

kombinator - en funksjon som samler resultatene av akkumulatoren. Combiner kalles bare i parallellmodus for å redusere resultatene av akkumulatorer fra forskjellige tråder.

Så la oss se på disse tre metodene i aksjon:

ValgfrittInt redusert = IntStream.range (1, 4) .reduser ((a, b) -> a + b);

redusert = 6 (1 + 2 + 3)

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

redusertToParams = 16 (10 + 1 + 2 + 3)

int redusertParams = Stream.of (1, 2, 3) .reduser (10, (a, b) -> a + b, (a, b) -> {log.info ("kombinator ble kalt"); returner en + b;});

Resultatet blir det samme som i forrige eksempel (16), og det vil ikke være noen innlogging, noe som betyr at den kombinereren ikke ble kalt. For å få en kombinator til å fungere, bør en strøm være parallell:

int redusertParallell = Arrays.asList (1, 2, 3) .parallelStream () .reduser (10, (a, b) -> a + b, (a, b) -> {log.info ("kombinator ble kalt" ); returner a + b;});

Resultatet her er annerledes (36) og kombinatoren ble kalt to ganger. Her fungerer reduksjonen med følgende algoritme: akkumulatoren kjørte tre ganger ved å legge til hvert element i strømmen til identitet til hvert element i strømmen. Disse handlingene gjøres parallelt. Som et resultat har de (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;). Nå kan kombinator slå sammen disse tre resultatene. Det trenger to iterasjoner for det (12 + 13 = 25; 25 + 11 = 36).

7.2. De samle inn() Metode

Reduksjon av en strøm kan også utføres av en annen terminaloperasjon - samle inn() metode. Den godtar et argument av typen Samler, som spesifiserer reduksjonsmekanismen. Det er allerede opprettet forhåndsdefinerte samlere for de vanligste operasjonene. De kan nås ved hjelp av Samlere type.

I denne delen vil vi bruke følgende Liste som kilde for alle strømmer:

Liste productList = Arrays.asList (nytt produkt (23, "poteter"), nytt produkt (14, "oransje"), nytt produkt (13, "sitron"), nytt produkt (23, "brød"), nytt produkt ( 13, "sukker"));

Konvertere en strøm til Samling (Samling, liste eller Sett):

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

Redusere til String:

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

De snekker () metoden kan ha fra en til tre parametere (skilletegn, prefiks, suffiks). Det handeste med å bruke snekker () - utvikler trenger ikke å sjekke om strømmen når slutten for å bruke suffikset og ikke for å bruke en skilletegn. Samler vil ta seg av det.

Behandler gjennomsnittsverdien for alle numeriske elementer i strømmen:

dobbelt gjennomsnittspris = produktListe.strøm (). samle (Collectors.averagingInt (Produkt :: getPrice));

Behandler summen av alle numeriske elementer i strømmen:

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

Metoder averagingXX (), summingXX () og summarizingXX () kan fungere som med primitiver (int, lang, dobbel) som med deres innpakningsklasser (Heltall, langt, dobbelt). En kraftigere funksjon ved disse metodene er å gi kartleggingen. Så utvikleren trenger ikke bruke et ekstra kart() drift før samle inn() metode.

Samle inn statistisk informasjon om strømens elementer:

IntSummaryStatistics statistikk = productList.stream () .collect (Collectors.summarizingInt (Produkt :: getPrice));

Ved å bruke den resulterende typen forekomst IntSummaryStatistics utvikler kan lage en statistisk rapport ved å søke toString () metode. Resultatet blir et String felles for denne “IntSummaryStatistics {count = 5, sum = 86, min = 13, average = 17,200000, max = 23}”.

Det er også enkelt å trekke ut separate verdier for dette objektet telle, sum, min, gjennomsnitt ved å anvende metoder getCount (), getSum (), getMin (), getAverage (), getMax (). Alle disse verdiene kan ekstraheres fra en enkelt rørledning.

Gruppering av strømens elementer i henhold til den angitte funksjonen:

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

I eksemplet ovenfor ble strømmen redusert til Kart som grupperer alle produktene etter pris.

Deler strømens elementer i grupper i henhold til noe predikat:

Kart mapPartioned = productList.stream () .collect (Collectors.partitioningBy (element -> element.getPrice ()> 15));

Å skyve samleren for å utføre ytterligere transformasjon:

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

I dette spesielle tilfellet har samleren konvertert en strøm til en Sett og deretter opprettet det umodifiserbare Sett ut av det.

Tilpasset samler:

Hvis det av en eller annen grunn skulle opprettes en tilpasset samler, er den enkleste og mindre detaljerte måten å gjøre det på - å bruke metoden av() av typen Samler.

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

I dette eksemplet er en forekomst av Samler ble redusert til LinkedList.

Parallelle bekker

Før Java 8 var parallellisering kompleks. Fremvoksende av ExecutorService og ForkJoin forenklet utviklernes liv litt, men de må fortsatt huske på hvordan man lager en spesifikk utfører, hvordan man kjører den og så videre.Java 8 introduserte en måte å oppnå parallellitet i en funksjonell stil.

API-en gjør det mulig å opprette parallelle strømmer som utfører operasjoner i parallellmodus. Når kilden til en strøm er en Samling eller en array det kan oppnås ved hjelp av parallelStream () metode:

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

Hvis kilden til strømmen er noe annet enn en Samling eller en array, den parallell() metoden skal brukes:

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

Under panseret bruker Stream API automatisk ForkJoin rammeverk for å utføre operasjoner parallelt. Som standard vil den felles trådgruppen brukes, og det er ingen måte (i det minste for øyeblikket) å tilordne noen tilpassede trådgrupper til den. Dette kan løses ved å bruke et tilpasset sett med parallelle samlere.

Når du bruker strømmer i parallellmodus, unngå å blokkere operasjoner og bruk parallellmodus når oppgaver trenger like lang tid å utføre (hvis en oppgave varer mye lenger enn den andre, kan den redusere appens arbeidsflyt).

Strømmen i parallellmodus kan konverteres tilbake til sekvensiell modus ved å bruke sekvensiell () metode:

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

Konklusjoner

Stream API er et kraftig, men enkelt å forstå sett med verktøy for behandling av sekvens av elementer. Det lar oss redusere en enorm mengde kokerplatekode, lage mer lesbare programmer og forbedre appens produktivitet når de brukes riktig.

I de fleste av kodeprøvene som vises i denne artikkelen, ble strømmer stående uforbrukede (vi brukte ikke Lukk() metode eller en terminaloperasjon). I en ekte app, ikke la en øyeblikkelig strøm strømme forbrukes, da det vil føre til minnelekkasjer.

De komplette kodeeksemplene som følger med artikkelen, er tilgjengelig på GitHub.


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