Java 8 Stream API Analogies i Kotlin

1. Introduksjon

Java 8 introduserte konseptet med Strømmer til samlingshierarkiet. Disse tillater veldig kraftig behandling av data på en veldig lesbar måte, ved å bruke noen funksjonelle programmeringskonsepter for å få prosessen til å fungere.

Vi vil undersøke hvordan vi kan oppnå den samme funksjonaliteten ved å bruke Kotlin-uttrykk. Vi vil også se på funksjoner som ikke er tilgjengelige i vanlig Java.

2. Java vs. Kotlin

I Java 8 kan den nye fancy API bare brukes når du kommuniserer med java.util.stream.Stream tilfeller.

Det gode er at alle standardkolleksjoner - alt som implementeres java.util.Collection - ha en bestemt metode strøm() som kan produsere en Strøm forekomst.

Det er viktig å huske at Strøm er ikke en Samling.Det implementeres ikke java.util.Collection og det implementerer ikke noen av de normale semantikkene i Samlinger i Java. Det ligner mer på engang Iterator ved at den er avledet fra en Samling og brukes til å jobbe gjennom det, utføre operasjoner på hvert element som sees.

I Kotlin støtter allerede alle samlingstyper disse operasjonene uten å måtte konvertere dem først. En konvertering er bare nødvendig hvis semantikken i samlingen er feil - f.eks Sett har unike elementer, men er uordnet.

En fordel med dette er at det ikke er behov for en første konvertering fra en Samling inn i en Strøm, og ikke behov for en endelig konvertering fra a Strøm tilbake til en samling - ved hjelp av samle inn() ringer.

For eksempel må vi i Java 8 skrive følgende:

someList .stream () .map () // noen operasjoner .collect (Collectors.toList ());

Tilsvarende i Kotlin er veldig enkelt:

someList .map () // noen operasjoner

I tillegg Java 8 Strømmer er ikke gjenbrukbare. Etter Strøm forbrukes, kan den ikke brukes igjen.

Følgende fungerer for eksempel ikke:

Stream someIntegers = integers.stream (); someIntegers.forEach (...); someIntegers.forEach (...); // et unntak

I Kotlin betyr det faktum at dette bare er normale samlinger at dette problemet aldri oppstår. Mellomtilstand kan tilordnes variabler og deles raskt, og fungerer bare som vi forventer.

3. Lat sekvenser

En av de viktigste tingene om Java 8 Strømmer er at de blir evaluert lat. Dette betyr at det ikke vil utføres mer arbeid enn nødvendig.

Dette er spesielt nyttig hvis vi gjør potensielt dyre operasjoner på elementene i Strøm, eller det gjør det mulig å jobbe med uendelige sekvenser.

For eksempel, IntStream.generate vil produsere en potensielt uendelig Strøm av heltall. Hvis vi ringer findFirst () på den, vil vi få det første elementet, og ikke løpe inn i en uendelig løkke.

I Kotlin er samlingene ivrige, heller enn late. Unntaket her er Sekvenssom evaluerer lat.

Dette er et viktig skille å merke seg, som følgende eksempel viser:

val result = listOf (1, 2, 3, 4, 5) .kart {n -> n * n} .filter {n -> n <10}. første ()

Kotlin-versjonen av dette vil utføre fem kart() operasjoner, fem filter() operasjoner og trekk deretter ut den første verdien. Java 8-versjonen vil bare utføre en kart() og en filter() fordi fra perspektivet til den siste operasjonen, er det ikke behov for mer.

Alle samlinger i Kotlin kan konverteres til en lat sekvens ved hjelp av asSequence () metode.

Bruker en Sekvens i stedet for en Liste i eksemplet ovenfor utfører samme antall operasjoner som i Java 8.

4. Java 8 Strøm Operasjoner

I Java 8, Strøm virksomheten er delt inn i to kategorier:

  • mellomliggende og
  • terminal

Mellomoperasjoner konverterer i hovedsak en Strøm inn i en annen lat - for eksempel en Strøm av alle heltall i a Strøm av alle jevne heltall.

Terminalalternativer er det siste trinnet i Strøm metodekjede og utløse selve behandlingen.

I Kotlin er det ikke noe slikt skille. I stedet, alt dette er bare funksjoner som tar samlingen som input og produserer en ny output.

Merk at hvis vi bruker en ivrig samling i Kotlin, blir disse operasjonene evaluert umiddelbart, noe som kan være overraskende sammenlignet med Java. Hvis vi trenger det for å være lat, husk å konvertere til en Sekvens først.

4.1. Mellomliggende operasjoner

Nesten alle mellomliggende operasjoner fra Java 8 Streams API har ekvivalenter i Kotlin. Dette er imidlertid ikke mellomliggende operasjoner - bortsett fra i tilfelle Sekvens klasse - ettersom de resulterer i fullbefolkede samlinger fra behandling av inngangssamlingen.

Ut av disse operasjonene er det flere som fungerer nøyaktig det samme - filter(), kart(), flatMap (), distinkt() og sortert () - og noen som bare fungerer med forskjellige navn - grense() er nå ta, og hopp over () er nå miste(). For eksempel:

val oddSquared = listOf (1, 2, 3, 4, 5). filter {n -> n% 2 == 1} // 1, 3, 5 .kart {n -> n * n} // 1, 9 , 25. Drop (1) // 9, 25. Ta (1) // 9

Dette vil returnere enkeltverdien “9” - 3².

Noen av disse operasjonene har også en ekstra versjon - suffiks med ordet "Til" - som kommer ut i en gitt samling i stedet for å produsere en ny.

Dette kan være nyttig for behandling av flere inngangssamlinger til samme utgangssamling, for eksempel:

val target = mutableList () listOf (1, 2, 3, 4, 5) .filterTo (target) {n -> n% 2 == 0}

Dette vil sette inn verdiene “2” og “4” i listen “mål”.

Den eneste operasjonen som normalt ikke har direkte erstatning er kikke () - brukes i Java 8 for å gjenta over oppføringene i Strøm midt i en prosesseringsrørledning uten å forstyrre strømmen.

Hvis vi bruker en lat Sekvens i stedet for en ivrig samling, så er det en på hver() funksjon som erstatter direkte kikke funksjon. Dette eksisterer bare på denne ene klassen, og derfor må vi være klar over hvilken type vi bruker for at den skal fungere.

Det er også noen ekstra variasjoner på standard mellomoperasjoner som gjør livet lettere. For eksempel filter operasjonen har flere versjoner filterNotNull (), filterIsInstance (), filterNot () og filterIndexed ().

For eksempel:

listOf (1, 2, 3, 4, 5) .kart {n -> n * (n + 1) / 2} .mapIndexed {(i, n) -> "Trekantetall $ i: $ n"}

Dette vil gi de første fem trekantede tallene, i form av “Trekantnummer 3: 6”

En annen viktig forskjell er i måten flatMap operasjonen fungerer. I Java 8 er denne operasjonen nødvendig for å returnere a Strøm for eksempel, mens det i Kotlin kan returnere hvilken som helst samlingstype. Dette gjør det lettere å jobbe med.

For eksempel:

val letters = listOf ("This", "Is", "An", "Example") .flatMap {w -> w.toCharArray ()} // Produserer et listefilter {c -> Character.isUpperCase (c) }

I Java 8 må den andre linjen pakkes inn Arrays.toStream () for at dette skal fungere.

4.2. Terminaloperasjoner

Alle standard terminaloperasjoner fra Java 8 Streams API har direkte erstatninger i Kotlin, med det eneste unntaket av samle inn.

Et par av dem har forskjellige navn:

  • anyMatch () ->noen()
  • allMatch () ->alle()
  • noneMatch () ->ingen()

Noen av dem har flere variasjoner å jobbe med hvordan Kotlin har forskjeller - det er det først() og firstOrNull (), hvor først kaster hvis samlingen er tom, men returnerer en ikke-nullbar type ellers.

Den interessante saken er samle inn. Java 8 bruker dette for å kunne samle alle Strøm elementer til en samling ved hjelp av en gitt strategi.

Dette gir mulighet for en vilkårlig Samler som skal leveres, som vil bli forsynt med hvert element i samlingen og vil produsere en produksjon av noe slag. Disse brukes fra Samlere hjelperklasse, men vi kan skrive våre egne om nødvendig.

I Kotlin er det direkte erstatninger for nesten alle standard samlere tilgjengelig direkte som medlemmer på selve samleobjektet - det er ikke behov for et ekstra trinn med oppsamleren.

Det eneste unntaket her er summarizingDouble/oppsummering/oppsummerer Lang metoder - som gir gjennomsnitt, telle, min, maks og sum alt på en gang. Hver av disse kan produseres individuelt - selv om det åpenbart har en høyere pris.

Alternativt kan vi administrere det ved hjelp av en for hver løkke og håndtere den for hånd om nødvendig - det er usannsynlig at vi trenger alle disse 5 verdiene samtidig, så vi trenger bare å implementere de som er viktige.

5. Ytterligere operasjoner i Kotlin

Kotlin legger til noen ekstra operasjoner i samlinger som ikke er mulig i Java 8 uten å implementere dem selv.

Noen av disse er ganske enkelt utvidelser av standardoperasjonene, som beskrevet ovenfor. Det er for eksempel mulig å gjøre alle operasjonene slik at resultatet blir lagt til en eksisterende samling i stedet for å returnere en ny samling.

Det er også mulig i mange tilfeller å ha lambda forsynt med ikke bare elementet det gjelder, men også indeksen til elementet - for samlinger som er bestilt, og så er indekser fornuftige.

Det er også noen operasjoner som eksplisitt utnytter nullsikkerheten til Kotlin - for eksempel; vi kan utføre en filterNotNull () på en Liste å returnere a Liste, der alle null blir fjernet.

Faktiske tilleggsoperasjoner som kan gjøres i Kotlin, men ikke i Java 8 Streams, inkluderer:

  • glidelås() og pakke ut () - brukes til å kombinere to samlinger i en sekvens av par, og omvendt for å konvertere en samling par til to samlinger
  • forbinder - brukes til å konvertere en samling til et kart ved å gi en lambda for å konvertere hver oppføring i samlingen til et nøkkel / verdipar i det resulterende kartet

For eksempel:

val numbers = listOf (1, 2, 3) val words = listOf ("one", "two", "three") numbers.zip (ord)

Dette gir en Liste, med verdier 1 til “en”, 2 til “to” og 3 til “tre”.

val kvadrater = listOf (1, 2, 3, 4,5). knytte {n -> n til n * n}

Dette gir en Kart, hvor tastene er tallene 1 til 5, og verdiene er kvadratene til disse verdiene.

6. Sammendrag

De fleste strømoperasjonene vi er vant til fra Java 8 er direkte brukbare i Kotlin på standard Collection-klasser, uten behov for å konvertere til en Strøm først.

I tillegg gir Kotlin mer fleksibilitet til hvordan dette fungerer, ved å legge til flere operasjoner som kan brukes og mer variasjon på eksisterende operasjoner.

Imidlertid er Kotlin ivrig som standard, ikke lat. Dette kan føre til at ytterligere arbeid utføres hvis vi ikke er forsiktige med samlingstypene som brukes.