Java 8 Stream API-analogen in Kotlin

1. Inleiding

Java 8 introduceerde het concept van Streams naar de collectiehiërarchie. Deze maken een zeer krachtige verwerking van gegevens op een zeer leesbare manier mogelijk, waarbij enkele functionele programmeerconcepten worden gebruikt om het proces te laten werken.

We zullen onderzoeken hoe we dezelfde functionaliteit kunnen bereiken door Kotlin-idiomen te gebruiken. We zullen ook kijken naar functies die niet beschikbaar zijn in gewoon Java.

2. Java versus Kotlin

In Java 8 kan de nieuwe fancy API alleen worden gebruikt bij interactie met java.util.stream.Stream gevallen.

Het goede is dat alle standaardcollecties - alles wat implementeert java.util.Collection - een bepaalde methode hebben stroom() die een Stroom voorbeeld.

Het is belangrijk om te onthouden dat de Stroom is geen Verzameling.Het implementeert niet java.util.Collection en het implementeert geen van de normale semantiek van Collecties in Java. Het lijkt meer op een eenmalig Iterator in die zin dat het is afgeleid van een Verzameling en wordt gebruikt om erdoorheen te werken, bewerkingen uit te voeren op elk element dat wordt gezien.

In Kotlin ondersteunen alle soorten collecties deze bewerkingen al zonder ze eerst te hoeven converteren. Een conversie is alleen nodig als de semantiek van de collectie onjuist is, bijvoorbeeld een Set heeft unieke elementen maar is ongeordend.

Een voordeel hiervan is dat er geen initiële conversie van een Verzameling in een Stroom, en geen definitieve conversie van een Stroom terug in een verzameling - met behulp van de verzamelen() oproepen.

In Java 8 zouden we bijvoorbeeld het volgende moeten schrijven:

someList .stream () .map () // enkele bewerkingen .collect (Collectors.toList ());

Het equivalent in Kotlin is heel eenvoudig:

someList .map () // enkele bewerkingen

Bovendien Java 8 Streams zijn ook niet herbruikbaar. Na Stroom wordt verbruikt, kan het niet opnieuw worden gebruikt.

Het volgende werkt bijvoorbeeld niet:

Stream someIntegers = integers.stream (); someIntegers.forEach (...); someIntegers.forEach (...); // een uitzondering

Bij Kotlin betekent het feit dat dit allemaal gewone verzamelingen zijn, dat dit probleem zich nooit voordoet. Tussenliggende status kan aan variabelen worden toegewezen en snel worden gedeeld, en werkt gewoon zoals we zouden verwachten.

3. Luie reeksen

Een van de belangrijkste dingen over Java 8 Streams is dat ze lui worden beoordeeld. Dit betekent dat er niet meer werk wordt verricht dan nodig is.

Dit is vooral handig als we potentieel dure bewerkingen uitvoeren op de elementen in het Stroom, of het maakt het mogelijk om met oneindige reeksen te werken.

Bijvoorbeeld, IntStream.generate zal een potentieel oneindig voortbrengen Stroom van gehele getallen. Als we bellen findFirst () daarop krijgen we het eerste element en komen we niet in een oneindige lus terecht.

In Kotlin zijn collecties gretig in plaats van lui. De uitzondering hierop is Volgorde, wat lui evalueert.

Dit is een belangrijk onderscheid om op te merken, zoals het volgende voorbeeld laat zien:

val result = listOf (1, 2, 3, 4, 5) .map {n -> n * n} .filter {n -> n <10} .first ()

De Kotlin-versie hiervan zal vijf uitvoeren kaart() operaties, vijf filter() bewerkingen en extraheer vervolgens de eerste waarde. De Java 8-versie zal er slechts één uitvoeren kaart() en een filter() want vanuit het perspectief van de laatste operatie is er niet meer nodig.

Alle collecties in Kotlin kunnen worden geconverteerd naar een luie reeks met behulp van de asSequence () methode.

Gebruik maken van een Volgorde inplaats van een Lijst voert in het bovenstaande voorbeeld hetzelfde aantal bewerkingen uit als in Java 8.

4. Java 8 Stroom Operaties

In Java 8, Stroom operaties zijn onderverdeeld in twee categorieën:

  • tussenliggende en
  • terminal

Tussenliggende bewerkingen converteren er in wezen één Stroom in een andere lui - bijvoorbeeld een Stroom van alle gehele getallen in een Stroom van alle even gehele getallen.

Terminalopties zijn de laatste stap van Stroom methodeketen en start de daadwerkelijke verwerking.

In Kotlin is er geen dergelijk onderscheid. In plaats daarvan, dit zijn allemaal slechts functies die de verzameling als invoer nemen en een nieuwe uitvoer produceren.

Merk op dat als we een gretige verzameling in Kotlin gebruiken, deze bewerkingen onmiddellijk worden geëvalueerd, wat verrassend kan zijn in vergelijking met Java. Als we het nodig hebben om lui te zijn, vergeet dan niet om te converteren naar een Volgorde eerste.

4.1. Tussenliggende operaties

Bijna alle tussenliggende bewerkingen van de Java 8 Streams API hebben equivalenten in Kotlin. Dit zijn echter geen tussenliggende bewerkingen - behalve in het geval van de Volgorde class - omdat ze resulteren in volledig gevulde verzamelingen door het verwerken van de invoercollectie.

Van deze bewerkingen zijn er verschillende die precies hetzelfde werken - filter(), kaart(), flatMap (), onderscheiden () en gesorteerd () - en sommige werken alleen hetzelfde met verschillende namen - limiet() is nu nemen, en overspringen() is nu laten vallen(). Bijvoorbeeld:

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

Dit retourneert de enkele waarde “9” - 3².

Sommige van deze bewerkingen hebben ook een extra versie - met het achtervoegsel "Naar" - dat resulteert in een verstrekte verzameling in plaats van een nieuwe te produceren.

Dit kan handig zijn voor het verwerken van meerdere inputcollecties in dezelfde outputcollectie, bijvoorbeeld:

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

Dit zal de waarden "2" en "4" in de lijst "target" invoegen.

De enige bewerking die normaal gesproken geen directe vervanging heeft, is kijkje() - gebruikt in Java 8 om de vermeldingen in het Stroom midden in een verwerkingspijpleiding zonder de stroom te onderbreken.

Als we een luie gebruiken Volgorde in plaats van een gretige verzameling, dan is er een op elke() functie die de kijkje functie. Dit bestaat echter alleen in deze ene klasse, en dus moeten we ons ervan bewust zijn welk type we gebruiken om het te laten werken.

Er zijn ook enkele aanvullende variaties op de standaard tussenliggende bewerkingen die het leven gemakkelijker maken. Bijvoorbeeld de filter operatie heeft extra versies filterNotNull (), filterIsInstance (), filterNot () en filterIndexed ().

Bijvoorbeeld:

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

Dit levert de eerste vijf driehoekige getallen op, in de vorm 'Driehoeksgetal 3: 6'

Een ander belangrijk verschil is de manier waarop de flatMap operatie werkt. In Java 8 is deze bewerking vereist om een Stroom bijvoorbeeld, terwijl het in Kotlin elk type verzameling kan retourneren. Dit maakt het gemakkelijker om mee te werken.

Bijvoorbeeld:

val letters = listOf ("This", "Is", "An", "Example") .flatMap {w -> w.toCharArray ()} // Produceert een lijst .filter {c -> Character.isUpperCase (c) }

In Java 8 zou de tweede regel moeten worden ingepakt Arrays.toStream () om dit te laten werken.

4.2. Terminaloperaties

Alle standaard Terminal Operations van de Java 8 Streams API hebben directe vervangingen in Kotlin, met als enige uitzondering verzamelen.

Een paar hebben verschillende namen:

  • anyMatch () ->ieder()
  • allMatch () ->alle()
  • noneMatch () ->geen()

Sommigen van hen hebben aanvullende variaties om mee te werken hoe Kotlin verschillen heeft - die is er eerste() en firstOrNull (), waar eerste gooit als de verzameling leeg is, maar retourneert anders een niet-nullabel type.

Het interessante geval is verzamelen. Java 8 gebruikt dit om alles te kunnen verzamelen Stroom elementen aan een verzameling met behulp van een verstrekte strategie.

Dit zorgt voor een willekeurige Verzamelaar worden verstrekt, dat bij elk element in de collectie wordt geleverd en een of andere output zal produceren. Deze worden gebruikt vanaf de Verzamelaars helper class, maar we kunnen onze eigen schrijven als dat nodig is.

In Kotlin zijn directe vervangers voor bijna alle standaard verzamelaars direct beschikbaar als lid op het verzamelobject zelf - er is geen extra trede nodig als de collector wordt geleverd.

De enige uitzondering hierop is de samenvattenDubbel/samenvattenInt/samenvattenLang methoden - die in één keer gemiddelde, aantal, min, max en som produceren. Elk van deze kan afzonderlijk worden geproduceerd - hoewel dat uiteraard hogere kosten met zich meebrengt.

Als alternatief kunnen we het beheren met behulp van een for-each-lus en deze indien nodig met de hand behandelen - het is onwaarschijnlijk dat we alle 5 deze waarden tegelijkertijd nodig hebben, dus we hoeven alleen de belangrijke te implementeren.

5. Aanvullende bewerkingen in Kotlin

Kotlin voegt enkele extra bewerkingen toe aan verzamelingen die niet mogelijk zijn in Java 8 zonder ze zelf te implementeren.

Sommige hiervan zijn gewoon uitbreidingen van de standaardbewerkingen, zoals hierboven beschreven. Het is bijvoorbeeld mogelijk om alle bewerkingen zo uit te voeren dat het resultaat wordt toegevoegd aan een bestaande collectie in plaats van een nieuwe collectie terug te sturen.

Het is in veel gevallen ook mogelijk om de lambda te laten voorzien van niet alleen het betreffende element, maar ook de index van het element - voor verzamelingen die geordend zijn, en dus zijn indexen zinvol.

Er zijn ook enkele bewerkingen die expliciet profiteren van de nulveiligheid van Kotlin - bijvoorbeeld; we kunnen een filterNotNull () op een Lijst om een Lijst, waar alle nullen worden verwijderd.

Werkelijke aanvullende bewerkingen die kunnen worden uitgevoerd in Kotlin maar niet in Java 8 Streams zijn onder meer:

  • zip () en uitpakken() - worden gebruikt om twee verzamelingen te combineren tot één reeks paren, en omgekeerd om een ​​verzameling paren om te zetten in twee verzamelingen
  • associëren - wordt gebruikt voor het converteren van een verzameling naar een kaart door een lambda op te geven om elk item in de verzameling om te zetten in een sleutel / waarde-paar in de resulterende kaart

Bijvoorbeeld:

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

Dit levert een Lijst, met waarden 1 tot "één", 2 tot "twee" en 3 tot "drie".

val squares = listOf (1, 2, 3, 4,5) .associate {n -> n naar n * n}

Dit levert een Kaart, waarbij de sleutels de nummers 1 tot en met 5 zijn, en de waarden de kwadraten van die waarden zijn.

6. Samenvatting

De meeste stream-bewerkingen die we gewend zijn van Java 8 zijn direct bruikbaar in Kotlin op de standaardcollectieklassen, zonder dat ze hoeven te worden geconverteerd naar een Stroom eerste.

Daarnaast voegt Kotlin meer flexibiliteit toe aan hoe dit werkt, door meer bewerkingen toe te voegen die kunnen worden gebruikt en meer variatie op de bestaande bewerkingen.

Kotlin is echter standaard gretig, niet lui. Dit kan leiden tot extra werkzaamheden als we niet opletten met de soorten collectie die worden gebruikt.