Analogies de l’API Java 8 Stream dans Kotlin

Analogies d'API Java 8 Stream dans Kotlin

1. introduction

Java 8 a introduit le concept deStreams dans la hiérarchie des collections. Celles-ci permettent un traitement très puissant des données de manière très lisible, en utilisant des concepts de programmation fonctionnels pour que le processus fonctionne.

Nous étudierons comment atteindre la même fonctionnalité en utilisant les idiomes de Kotlin. Nous examinerons également les fonctionnalités qui ne sont pas disponibles en Java.

2. Java vs. Kotlin

Dans Java 8, la nouvelle API sophistiquée ne peut être utilisée que lors de l'interaction avec les instancesjava.util.stream.Stream.

La bonne chose est que toutes les collections standard - tout ce qui implémentejava.util.Collection - ont une méthode particulièrestream() qui peut produire une instanceStream.

Il est important de se rappeler que leStream n'est pas unCollection.It does not implement java.util.Collection and it does not implement any of the normal semantics of Collections in Java. Il s'apparente davantage à unIterator ponctuel en ce qu'il est dérivé d'unCollection et est utilisé pour le parcourir, en effectuant des opérations sur chaque élément visible.

In Kotlin, all collection types already support these operations sans avoir besoin de les convertir au préalable. Une conversion n'est nécessaire que si la sémantique de la collection est incorrecte - par exemple, unSet a des éléments uniques mais n'est pas ordonné.

Un avantage de ceci est qu'il n'y a pas besoin d'une conversion initiale d'unCollection en unStream, et pas besoin d'une conversion finale d'unStream en une collection - en utilisant lecollect() appels.

Par exemple, en Java 8, il faudrait écrire ce qui suit:

someList
  .stream()
  .map() // some operations
  .collect(Collectors.toList());

L'équivalent en Kotlin est très simplement:

someList
  .map() // some operations

Additionally, Java 8 Streams are also non-reusable. Une foisStream consommé, il ne peut plus être utilisé.

Par exemple, ce qui suit ne fonctionnera pas:

Stream someIntegers = integers.stream();
someIntegers.forEach(...);
someIntegers.forEach(...); // an exception

Chez Kotlin, le fait que ce soient toutes des collections normales signifie que ce problème ne se pose jamais. Intermediate state can be assigned to variables and shared quickly, et fonctionne juste comme on s'y attend.

3. Séquences paresseuses

L'un des éléments clés de Java 8Streams est qu'ils sont évalués paresseusement. Cela signifie que pas plus de travail que nécessaire ne sera effectué.

Ceci est particulièrement utile si nous faisons des opérations potentiellement coûteuses sur les éléments dans lesStream, ou si cela permet de travailler avec des séquences infinies.

Par exemple,IntStream.generate produira unStream potentiellement infini d'entiers. Si nous appelonsfindFirst() dessus, nous obtiendrons le premier élément, et nous ne courrons pas dans une boucle infinie.

In Kotlin, collections are eager, rather than lazy. L'exception ici estSequence, qui évalue paresseusement.

Il est important de noter cette distinction, comme le montre l'exemple suivant:

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

La version Kotlin de cela effectuera cinq opérationsmap(), cinq opérationsfilter(), puis extraira la première valeur. La version Java 8 n'effectuera qu'unmap() et unfilter() car du point de vue de la dernière opération, il n'en faut plus.

All collections in Kotlin can be converted to a lazy sequence using the asSequence() method.

L'utilisation d'unSequence au lieu d'unList dans l'exemple ci-dessus effectue le même nombre d'opérations que dans Java 8.

4. Opérations Java 8Stream

Dans Java 8, les opérationsStream sont divisées en deux catégories:

  • intermédiaire et

  • Terminal

Les opérations intermédiaires convertissent essentiellement unStream en un autre paresseusement - par exemple, unStream de tous les entiers en unStream de tous les entiers pairs.

Les options de terminal sont la dernière étape de la chaîne de méthodesStream et déclenchent le traitement réel.

À Kotlin, cette distinction n’existe pas. Au lieu de cela,these are all just functions that take the collection as input and produce a new output.

Notez que si nous utilisons une collection impatiente dans Kotlin, ces opérations sont évaluées immédiatement, ce qui peut être surprenant par rapport à Java. If we need it to be lazy, remember to convert to a Sequence first.

4.1. Opérations intermédiaires

Almost all intermediate operations from the Java 8 Streams API have equivalents in Kotlin. Cependant, ce ne sont pas des opérations intermédiaires - sauf dans le cas de la classeSequence - car elles entraînent des collections entièrement remplies à partir du traitement de la collection d'entrée.

Parmi ces opérations, il y en a plusieurs qui fonctionnent exactement de la même manière -filter(),map(),flatMap(),distinct() etsorted() - et certaines fonctionnent le idem uniquement avec des noms différents -limit() est maintenanttake, etskip() est maintenantdrop(). Par exemple:

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
  .take(1) // 9

Cela retournera la valeur unique “9” - 3².

Some of these operations also have an additional version – suffixed with the word “To” - qui sort dans une collection fournie au lieu d'en produire une nouvelle.

Cela peut être utile pour traiter plusieurs collections d'entrée dans la même collection de sortie, par exemple:

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

Cela insérera les valeurs «2» et «4» dans la liste «cible».

The only operation that does not normally have a direct replacement is peek() - utilisé dans Java 8 pour parcourir les entrées dans lesStream au milieu d'un pipeline de traitement sans interrompre le flux.

Si nous utilisons unSequence paresseux au lieu d'une collection impatiente, alors il existe une fonctiononEach() qui remplace directement la fonctionpeek. Cela n’existe que sur cette classe, et nous devons donc savoir quel type nous utilisons pour que cela fonctionne.

There are also some additional variations on the standard intermediate operations that make life easier. Par exemple, l'opérationfilter a des versions supplémentairesfilterNotNull(),filterIsInstance(),filterNot() etfilterIndexed().

Par exemple:

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

Cela produira les cinq premiers nombres triangulaires, sous la forme «Nombre triangulaire 3: 6».

Une autre différence importante réside dans le fonctionnement de l'opérationflatMap. Dans Java 8, cette opération est nécessaire pour renvoyer une instanceStream, alors que dans Kotlin, elle peut renvoyer n'importe quel type de collection. Cela facilite le travail.

Par exemple:

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

Dans Java 8, la deuxième ligne devrait être encapsulée dansArrays.toStream() pour que cela fonctionne.

4.2. Opérations terminales

Toutes les opérations de terminal standard de l'API Java 8 Streams ont des remplacements directs dans Kotlin, à la seule exception decollect.

Quelques uns ont des noms différents:

  • anyMatch()any()

  • allMatch()all()

  • noneMatch()none()

Certains d'entre eux ont des variantes supplémentaires pour travailler avec la façon dont Kotlin a des différences - il y afirst() etfirstOrNull(), oùfirst lance si la collection est vide, mais retourne un type non nullable sinon.

Le cas intéressant estcollect. Java 8 l'utilise pour pouvoir collecter tous les élémentsStream dans une collection en utilisant une stratégie fournie.

Cela permet de fournir unCollector arbitraire, qui sera fourni avec chaque élément de la collection et produira une sortie quelconque. Ceux-ci sont utilisés à partir de la classe d'assistanceCollectors, mais nous pouvons écrire les nôtres si nécessaire.

In Kotlin there are direct replacements for almost all of the standard collectors available directly as members on the collection object itself - il n'y a pas besoin d'une étape supplémentaire avec le collecteur fourni.

La seule exception ici est les méthodessummarizingDouble /summarizingInt /summarizingLong - qui produisent la moyenne, le nombre, le min, le max et la somme en une seule fois. Chacun de ceux-ci peut être produit individuellement - bien que cela ait évidemment un coût plus élevé.

Alternativement, nous pouvons le gérer en utilisant une boucle for-each et le gérer à la main si nécessaire -it is unlikely we will need all 5 of these values at the same time, so we only need to implement the ones that are important.

5. Opérations supplémentaires à Kotlin

Kotlin ajoute des opérations supplémentaires aux collections qui ne sont pas possibles dans Java 8 sans les implémenter nous-mêmes.

Certaines d'entre elles ne sont que des extensions des opérations standard décrites ci-dessus. Par exemple, il est possible d'effectuer toutes les opérations de sorte que le résultat soit ajouté à une collection existante plutôt que de renvoyer une nouvelle collection.

Il est également possible que, dans de nombreux cas, le lambda soit fourni non seulement avec l'élément en question, mais également avec l'index de l'élément - pour les collections ordonnées, ce qui donne un sens aux index.

Il existe également des opérations qui tirent explicitement parti de la sécurité nulle de Kotlin - par exemple; nous pouvons effectuer unfilterNotNull() sur unList<String?> pour renvoyer unList<String>, où tous les nuls sont supprimés.

Les opérations supplémentaires pouvant être effectuées dans Kotlin mais pas dans Java 8 Streams incluent:

  • zip() etunzip() - sont utilisés pour combiner deux collections en une séquence de paires, et inversement pour convertir une collection de paires en deux collections

  • associate - est utilisé pour convertir une collection en une carte en fournissant un lambda pour convertir chaque entrée de la collection en une paire clé / valeur dans la carte résultante

Par exemple:

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

Cela produit unList<Pair<Int, String>>, avec des valeurs1 to “one”, 2 to “two” et3 to “three”.

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

Cela produit unMap<Int, Int>, où les clés sont les nombres 1 à 5, et les valeurs sont les carrés de ces valeurs.

6. Sommaire

La plupart des opérations de flux auxquelles nous sommes habitués à partir de Java 8 sont directement utilisables dans Kotlin sur les classes de collection standard, sans avoir besoin de convertir d'abord enStream.

En outre, Kotlin ajoute une plus grande flexibilité à la façon dont cela fonctionne, en ajoutant plus d'opérations pouvant être utilisées et plus de variation sur les opérations existantes.

Cependant, Kotlin est désireux par défaut, pas paresseux. Cela peut entraîner un travail supplémentaire si nous ne faisons pas attention aux types de collection utilisés.