Java 8 Stream API-vejledning

1. Oversigt

I denne dybdegående vejledning gennemgår vi den praktiske brug af Java 8 Streams fra oprettelse til parallel udførelse.

For at forstå dette materiale skal læsere have en grundlæggende viden om Java 8 (lambda-udtryk, Valgfri, metodehenvisninger) og af Stream API. Hvis du ikke er fortrolig med disse emner, skal du kigge på vores tidligere artikler - Nye funktioner i Java 8 og Introduktion til Java 8 Streams.

2. Stream Creation

Der er mange måder at oprette en streaminstans af forskellige kilder på. Når oprettelsen er forekommet vil ikke ændre sin kilde, tillader derfor oprettelse af flere forekomster fra en enkelt kilde.

2.1. Tom strøm

Det tom() metoden skal bruges i tilfælde af oprettelse af en tom strøm:

Stream streamEmpty = Stream.empty ();

Det er ofte tilfældet, at tom() metode bruges ved oprettelsen for at undgå at vende tilbage nul til streams uden element:

public Stream streamOf (List list) returliste == null 

2.2. Stream af Kollektion

Stream kan også oprettes af enhver type Kollektion (Samling, liste, sæt):

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å oprettes af et eksisterende array eller af en del af et array:

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

2.4. Stream.builder ()

Når bygherre bruges den ønskede type skal desuden specificeres i den rigtige del af udsagnet, ellers bygge () metoden opretter en forekomst af Strøm:

Stream streamBuilder = Stream.builder (). Tilføj ("a"). Tilføj ("b"). Tilføj ("c"). Build ();

2.5. Stream.generate ()

Det frembringe() metode accepterer en Leverandør til elementgenerering. Da den resulterende strøm er uendelig, skal udvikleren angive den ønskede størrelse eller frembringe() metode fungerer, indtil den når hukommelsesgrænsen:

Stream streamGenerated = Stream.generate (() -> "element"). Grænse (10);

Koden ovenfor opretter en sekvens på ti strenge med værdien - "element".

2.6. Stream.iterate ()

En anden måde at skabe en uendelig strøm på er ved hjælp af iterate () metode:

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

Det første element i den resulterende strøm er en første parameter for iterate () metode. For at oprette hvert følgende element anvendes den angivne funktion til det forrige element. I eksemplet ovenfor vil det andet element være 42.

2.7. Strøm af primitiver

Java 8 giver mulighed for at oprette streams ud af tre primitive typer: int, lang og dobbelt. Som Strøm er en generisk grænseflade, og der er ingen måde at bruge primitiver som en typeparameter med generiske, der blev oprettet tre nye specielle grænseflader: IntStream, LongStream, DoubleStream.

Brug af de nye grænseflader letter unødvendig automatisk boksning giver øget produktivitet:

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

Det rækkevidde (int startInclusive, int endExclusive) metoden opretter en ordnet stream fra den første parameter til den anden parameter. Det forøger værdien af ​​efterfølgende elementer med trinnet lig med 1. Resultatet inkluderer ikke den sidste parameter, det er kun en øvre grænse for sekvensen.

Det rangeClosed (int startInclusive, int endInclusive)metode gør det samme med kun en forskel - det andet element er inkluderet. Disse to metoder kan bruges til at generere en hvilken som helst af de tre typer primitivstrømme.

Siden Java 8 Tilfældig klasse giver en bred vifte af metoder til generering af primitiver. For eksempel opretter følgende kode en DoubleStream, som har tre elementer:

Tilfældig tilfældig = ny tilfældig (); DoubleStream doubleStream = tilfældig. Fordobler (3);

2.8. Stream af Snor

Snor kan også bruges som kilde til oprettelse af en stream.

Ved hjælp af tegn () metode til Snor klasse. Da der ikke er nogen grænseflade CharStream i JDK, the IntStream bruges til at repræsentere en strøm af tegn i stedet.

IntStream streamOfChars = "abc" .chars ();

Følgende eksempel bryder a Snor i understrenge i henhold til specificeret RegEx:

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

2.9. Strøm af fil

Java NIO klasse Filer tillader at generere en Strøm af en tekstfil gennem linjer () metode. Hver linje i teksten bliver et element i strømmen:

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

Det Charset kan specificeres som et argument for linjer () metode.

3. Henvisning til en strøm

Det er muligt at instantiere en stream og have en tilgængelig reference til den, så længe kun mellemliggende operationer blev kaldt. Udførelse af en terminaloperation gør en strøm utilgængelig.

For at demonstrere dette glemmer vi et stykke tid, at den bedste praksis er at kæde driftssekvensen. Udover sin unødvendige bredhed, er følgende kode teknisk gyldig:

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

Men et forsøg på at genbruge den samme reference efter at have ringet til terminaloperationen vil udløse IllegalStateException:

Valgfri firstElement = stream.findFirst ();

Som den IllegalStateException er en RuntimeException, vil en compiler ikke signalere om et problem. Så det er meget vigtigt at huske det Java 8 streams kan ikke genbruges.

Denne form for adfærd er logisk, fordi streams blev designet til at give en evne til at anvende en endelig rækkefølge af operationer til kilden til elementer i en funktionel stil, men ikke til at gemme elementer.

Så for at få tidligere kode til at fungere korrekt, skal der foretages nogle ændringer:

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

4. Stream rørledning

For at udføre en sekvens af operationer over elementerne i datakilden og samle deres resultater er der behov for tre dele - kilde, mellemliggende operation (er) og en terminal drift.

Mellemliggende operationer returnerer en ny modificeret stream. For eksempel at oprette en ny strøm af den eksisterende uden få elementer springe() metoden skal anvendes:

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

Hvis der er behov for mere end en ændring, kan mellemliggende operationer lænkes. Antag, at vi også er nødt til at erstatte alle strømelementer Strøm med en understreng af de første par tegn. Dette gøres ved at sammenkæde springe() og kort() metoder:

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

Som du kan se, er kort() metoden tager et lambda-udtryk som parameter. Hvis du vil lære mere om lambdas, skal du kigge på vores vejledning Lambda-udtryk og funktionelle grænseflader: tip og bedste praksis.

En stream i sig selv er værdiløs, den virkelige ting, som en bruger er interesseret i, er et resultat af terminaloperationen, som kan være en værdi af en eller anden type eller en handling, der anvendes på hvert element i strømmen. Der kan kun bruges en terminaloperation pr. Stream.

Den rigtige og mest bekvemme måde at bruge streams er på en stream pipeline, som er en kæde af streamkilde, mellemliggende operationer og en terminaloperation. For eksempel:

Liste liste = Arrays.asList ("abc1", "abc2", "abc3"); lang størrelse = liste.strøm (). spring over (1). kort (element -> element.substring (0, 3)). sorteret (). antal ();

5. Lazy Invocation

Mellemliggende operationer er dovne. Det betyder at de vil kun blive påberåbt, hvis det er nødvendigt for terminaloperationens udførelse.

For at demonstrere dette, forestil dig at vi har en metode blev kaldt(), som øger en indre tæller hver gang den blev kaldt:

privat lang tæller; privat tomrum blev kaldt () {tæller ++; }

Lad os kalde metode varHedder() fra drift filter():

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

Da vi har en kilde til tre elementer, kan vi antage denne metode filter() kaldes tre gange, og værdien af tæller variabel vil være 3. Men kørsel af denne kode ændres ikke tæller overhovedet er det stadig nul, så filter() metoden blev ikke kaldt engang. Årsagen til - mangler terminaloperationen.

Lad os omskrive denne kode lidt ved at tilføje en kort() drift og terminaloperation - findFirst (). Vi tilføjer også en evne til at spore en rækkefølge af metodeopkald ved hjælp af logning:

Valgfri stream = list.stream (). Filter (element -> {log.info ("filter () blev kaldt"); return element.contains ("2");}). Map (element -> {log.info ("map () blev kaldt"); return element.toUpperCase ();}). findFirst ();

Resulterende log viser, at filter() metoden blev kaldt to gange og kort() metode kun en gang. Det skyldes, at rørledningen udføres lodret. I vores eksempel opfyldte det første element i strømmen ikke filterets prædikat, derefter filter() metode blev påkaldt for det andet element, der passerede filteret. Uden at ringe til filter() for tredje element gik vi ned gennem rørledningen til kort() metode.

Det findFirst () operationen opfylder kun et element. Så i dette særlige eksempel tillod den dovne påkaldelse at undgå to metodeopkald - en til filter() og en til kort().

6. Orden for udførelse

Fra præstationssynspunktet den rigtige rækkefølge er et af de vigtigste aspekter af kædeoperationer i strømrørledningen:

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

Udførelse af denne kode øger tællerens værdi med tre. Dette betyder, at kort() metoden til strømmen blev kaldt tre gange. Men værdien af størrelse er en. Så den resulterende stream har kun et element, og vi udførte det dyre kort() operationer uden grund to gange ud af tre gange.

Hvis vi ændrer rækkefølgen af springe() og kort() metoder, det tæller vil kun stige med en. Så metoden kort() kaldes kun en gang:

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

Dette bringer os op til reglen: mellemliggende operationer, der reducerer strømmen, skal placeres før operationer, der gælder for hvert element. Så hold sådanne metoder som skip (), filter (), distinkt () øverst på din stream-pipeline.

7. Strømreduktion

API'et har mange terminaloperationer, som samler en stream til en type eller til en primitiv, for eksempel count (), max (), min (), sum (), men disse operationer fungerer i henhold til den foruddefinerede implementering. Og hvad hvis en udvikler har brug for at tilpasse en Streams reduktionsmekanisme? Der er to metoder, der gør det muligt at gøre dette - reducere()og indsamle() metoder.

7.1. Det reducere() Metode

Der er tre variationer af denne metode, som adskiller sig efter deres underskrifter og returnerende typer. De kan have følgende parametre:

identitet - den oprindelige værdi for en akkumulator eller en standardværdi, hvis en stream er tom, og der ikke er noget at akkumulere;

akkumulator - en funktion, der specificerer en logik for sammenlægning af elementer. Da akkumulator skaber en ny værdi for hvert reduktionstrin, svarer mængden af ​​nye værdier til strømens størrelse, og kun den sidste værdi er nyttig. Dette er ikke særlig godt for forestillingen.

kombinator - en funktion, der samler resultaterne af akkumulatoren. Combiner kaldes kun i en parallel tilstand for at reducere resultaterne af akkumulatorer fra forskellige tråde.

Så lad os se på disse tre metoder i aktion:

ValgfriInt reduceret = IntStream.range (1, 4) .reducer ((a, b) -> a + b);

reduceret = 6 (1 + 2 + 3)

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

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

int reduceretParams = Stream.of (1, 2, 3) .reducer (10, (a, b) -> a + b, (a, b) -> {log.info ("combiner blev kaldt"); returner en + b;});

Resultatet vil være det samme som i det foregående eksempel (16), og der vil ikke være noget login, hvilket betyder, at denne kombinator ikke blev kaldt. For at få en kombinator til at fungere, skal en stream være parallel:

int reduceretParallel = Arrays.asList (1, 2, 3) .parallelStream () .reducer (10, (a, b) -> a + b, (a, b) -> {log.info ("combiner blev kaldt" ); returner a + b;});

Resultatet her er anderledes (36), og kombinereren blev kaldt to gange. Her fungerer reduktionen ved hjælp af følgende algoritme: akkumulator kørte tre gange ved at tilføje hvert element i strømmen til identitet til hvert element i strømmen. Disse handlinger udføres parallelt. Som et resultat har de (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;). Nu kan combiner flette disse tre resultater. Det har brug for to iterationer til det (12 + 13 = 25; 25 + 11 = 36).

7.2. Det indsamle() Metode

Reduktion af en stream kan også udføres af en anden terminaloperation - indsamle() metode. Det accepterer et argument af typen Samler, som specificerer reduktionsmekanismen. Der er allerede oprettet foruddefinerede samlere til de mest almindelige operationer. De kan tilgås ved hjælp af Samlere type.

I dette afsnit bruger vi følgende Liste som kilde til alle streams:

Liste productList = Arrays.asList (nyt produkt (23, "kartofler"), nyt produkt (14, "orange"), nyt produkt (13, "citron"), nyt produkt (23, "brød"), nyt produkt ( 13, "sukker"));

Konvertering af en stream til Kollektion (Samling, liste eller Sæt):

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

Reducerer til Snor:

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

Det snedker () Metoden kan have fra en til tre parametre (afgrænser, præfiks, suffiks). Den nemmeste ting ved at bruge snedker () - udvikler behøver ikke at kontrollere, om strømmen når sin slutning for at anvende suffikset og ikke anvende en afgrænser. Samler vil tage sig af det.

Behandling af gennemsnitsværdien af ​​alle numeriske elementer i strømmen:

dobbelt gennemsnitPris = productList.stream () .collect (Collectors.averagingInt (Produkt :: getPrice));

Behandling af summen af ​​alle numeriske elementer i strømmen:

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

Metoder gennemsnit XX (), summingXX () og summarizingXX () kan fungere som med primitiver (int, lang, dobbelt) som med deres indpakningsklasser (Heltal, langt, dobbelt). En mere kraftfuld funktion ved disse metoder er at give kortlægningen. Så udvikleren behøver ikke bruge et ekstra kort() før driften indsamle() metode.

Indsamling af statistiske oplysninger om strømens elementer:

IntSummaryStatistics-statistik = productList.stream () .collect (Collectors.summarizingInt (Product :: getPrice));

Ved at bruge den resulterende forekomst af typen IntSummaryStatistics udvikleren kan oprette en statistisk rapport ved at anvende toString () metode. Resultatet bliver et Snor fælles for denne "IntSummaryStatistics {count = 5, sum = 86, min = 13, gennemsnit = 17.200.000, max = 23}".

Det er også let at udtrække separate værdier for dette objekt fra tælle, sum, min, gennemsnit ved at anvende metoder getCount (), getSum (), getMin (), getAverage (), getMax (). Alle disse værdier kan udvindes fra en enkelt pipeline.

Gruppering af strømens elementer i henhold til den specificerede funktion:

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

I eksemplet ovenfor blev strømmen reduceret til Kort der grupperer alle produkter efter deres pris.

Opdeling af strømens elementer i grupper efter et eller andet prædikat:

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

Skubber samleren til at udføre yderligere transformation:

Sæt unmodifiableSet = productList.stream () .collect (Collectors.collectingAndThen (Collectors.toSet (), Collections :: unmodifiableSet));

I dette særlige tilfælde har samleren konverteret en stream til en Sæt og skabte derefter det umodificerbare Sæt ud af det.

Brugerdefineret samler:

Hvis der af en eller anden grund skulle oprettes en brugerdefineret samler, den mest nemmeste og mindre detaljerede måde at gøre det på - er at bruge metoden af() af 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 eksempel er en forekomst af Samler blev reduceret til LinkedList.

Parallelle strømme

Før Java 8 var parallelisering kompleks. Fremkomst af ExecutorService og ForkTilmeld dig forenklet udviklerens liv en smule, men de skal stadig huske, hvordan man opretter en bestemt eksekutor, hvordan man kører det og så videre.Java 8 introducerede en måde at opnå parallelisme på i en funktionel stil.

API'en tillader oprettelse af parallelle streams, der udfører operationer i en parallel tilstand. Når kilden til en stream er en Kollektion eller en array det kan opnås ved hjælp af 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øm er noget andet end en Kollektion eller en array, det parallel() metoden skal anvendes:

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

Under hætten bruger Stream API automatisk ForkTilmeld dig ramme til at udføre operationer parallelt. Som standard vil den fælles trådpulje blive brugt, og der er ingen måde (i det mindste for nu) at tildele en brugerdefineret trådpulje til den. Dette kan overvindes ved hjælp af et tilpasset sæt parallelle samlere.

Når du bruger streams i parallel tilstand, skal du undgå at blokere handlinger og bruge parallel mode, når opgaver har brug for den samme tid at udføre (hvis en opgave varer meget længere end den anden, kan den sænke hele appens arbejdsgang).

Strømmen i parallel tilstand kan konverteres tilbage til den sekventielle tilstand ved hjælp af sekventiel () metode:

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

Konklusioner

Stream API er et kraftfuldt, men letforståeligt sæt værktøjer til behandling af sekvens af elementer. Det giver os mulighed for at reducere en enorm mængde kedelpladekode, skabe mere læsbare programmer og forbedre appens produktivitet, når de bruges korrekt.

I de fleste af kodeprøverne vist i denne artikel blev streams efterladt uforbrugt (vi anvendte ikke tæt() metode eller en terminaloperation). I en rigtig app, Efterlad ikke et øjeblikkeligt streams uforbrugt, da det vil føre til hukommelseslækage.

De komplette kodeeksempler, der ledsager artiklen, er tilgængelige på GitHub.