Quels équivalents Java 8 Stream.collect sont disponibles dans la bibliothèque standard de Kotlin?

181

En Java 8, il existe Stream.collectce qui permet des agrégations sur des collections. Dans Kotlin, cela n'existe pas de la même manière, à part peut-être comme une collection de fonctions d'extension dans la stdlib. Mais il n'est pas clair quelles sont les équivalences pour différents cas d'utilisation.

Par exemple, en haut du JavaDoc pour seCollectors trouvent des exemples écrits pour Java 8, et lorsque vous les portez vers Kolin, vous ne pouvez pas utiliser les classes Java 8 lorsque vous utilisez une version JDK différente, ils devraient donc être écrits différemment.

En termes de ressources en ligne montrant des exemples de collections Kotlin, elles sont généralement triviales et ne se comparent pas vraiment aux mêmes cas d'utilisation. Quels sont les bons exemples qui correspondent vraiment aux cas tels que ceux documentés pour Java 8 Stream.collect? La liste y est:

  • Accumuler les noms dans une liste
  • Accumuler les noms dans un TreeSet
  • Convertir les éléments en chaînes et les concaténer, séparés par des virgules
  • Calculer la somme des salaires de l'employé
  • Regrouper les collaborateurs par département
  • Calculer la somme des salaires par département
  • Partitionner les élèves en réussissant et en échec

Avec des détails dans le JavaDoc lié ci-dessus.

Remarque: cette question est intentionnellement écrite et répondue par l'auteur ( Questions auto-répondues ), de sorte que les réponses idiomatiques aux sujets Kotlin les plus fréquemment posées soient présentes dans SO. Aussi pour clarifier certaines réponses vraiment anciennes écrites pour les alphas de Kotlin qui ne sont pas exactes pour Kotlin d'aujourd'hui.

Jayson Minard
la source
Dans les cas où vous n'avez pas d'autre choix que d'utiliser collect(Collectors.toList())ou similaire, vous pouvez rencontrer ce problème: stackoverflow.com/a/35722167/3679676 (le problème, avec des solutions de contournement)
Jayson Minard

Réponses:

257

Il existe des fonctions dans la bibliothèque standard de Kotlin pour la moyenne, le nombre, la distinction, le filtrage, la recherche, le regroupement, la jonction, le mappage, min, max, le partitionnement, le découpage, le tri, la sommation, vers / depuis des tableaux, vers / depuis des listes, vers / depuis des cartes , union, co-itération, tous les paradigmes fonctionnels, et plus encore. Vous pouvez donc les utiliser pour créer de petits 1-liners et il n'est pas nécessaire d'utiliser la syntaxe plus compliquée de Java 8.

Je pense que la seule chose qui manque dans la Collectorsclasse Java 8 intégrée est la synthèse (mais dans une autre réponse à cette question est une solution simple) .

Une chose qui manque dans les deux est le traitement par lots, qui est vu dans une autre réponse de Stack Overflow et a également une réponse simple. Un autre cas intéressant est celui de Stack Overflow: manière idiomatique de renverser la séquence en trois listes en utilisant Kotlin . Et si vous souhaitez créer quelque chose comme Stream.collectdans un autre but, consultez Stream.collect personnalisé dans Kotlin

EDIT 11.08.2017: Les opérations de collecte par blocs / fenêtres ont été ajoutées dans kotlin 1.2 M2, voir https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


Il est toujours bon d'explorer la référence API pour kotlin.collections dans son ensemble avant de créer de nouvelles fonctions qui pourraient déjà y exister.

Voici quelques conversions d' Stream.collectexemples Java 8 vers l'équivalent dans Kotlin:

Accumuler les noms dans une liste

// Java:  
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
// Kotlin:
val list = people.map { it.name }  // toList() not needed

Convertir les éléments en chaînes et les concaténer, séparés par des virgules

// Java:
String joined = things.stream()
                       .map(Object::toString)
                       .collect(Collectors.joining(", "));
// Kotlin:
val joined = things.joinToString(", ")

Calculer la somme des salaires de l'employé

// Java:
int total = employees.stream()
                      .collect(Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val total = employees.sumBy { it.salary }

Regrouper les collaborateurs par département

// Java:
Map<Department, List<Employee>> byDept
     = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment));
// Kotlin:
val byDept = employees.groupBy { it.department }

Calculer la somme des salaires par département

// Java:
Map<Department, Integer> totalByDept
     = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                     Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}

Partitionner les élèves en réussissant et en échec

// Java:
Map<Boolean, List<Student>> passingFailing =
     students.stream()
             .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
// Kotlin:
val passingFailing = students.partition { it.grade >= PASS_THRESHOLD }

Noms des membres masculins

// Java:
List<String> namesOfMaleMembers = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(p -> p.getName())
    .collect(Collectors.toList());
// Kotlin:
val namesOfMaleMembers = roster.filter { it.gender == Person.Sex.MALE }.map { it.name }

Grouper les noms des membres de la liste par sexe

// Java:
Map<Person.Sex, List<String>> namesByGender =
      roster.stream().collect(
        Collectors.groupingBy(
            Person::getGender,                      
            Collectors.mapping(
                Person::getName,
                Collectors.toList())));
// Kotlin:
val namesByGender = roster.groupBy { it.gender }.mapValues { it.value.map { it.name } }   

Filtrer une liste vers une autre liste

// Java:
List<String> filtered = items.stream()
    .filter( item -> item.startsWith("o") )
    .collect(Collectors.toList());
// Kotlin:
val filtered = items.filter { it.startsWith('o') } 

Recherche de la chaîne la plus courte d'une liste

// Java:
String shortest = items.stream()
    .min(Comparator.comparing(item -> item.length()))
    .get();
// Kotlin:
val shortest = items.minBy { it.length }

Comptage des éléments dans une liste après l'application du filtre

// Java:
long count = items.stream().filter( item -> item.startsWith("t")).count();
// Kotlin:
val count = items.filter { it.startsWith('t') }.size
// but better to not filter, but count with a predicate
val count = items.count { it.startsWith('t') }

et ainsi de suite ... Dans tous les cas, aucune fonctionnalité spéciale de pliage, de réduction ou autre n'était requise pour imiter Stream.collect. Si vous avez d'autres cas d'utilisation, ajoutez-les dans les commentaires et nous pouvons voir!

À propos de la paresse

Si vous souhaitez traiter paresseusement une chaîne, vous pouvez la convertir en Sequenceutilisation asSequence()avant la chaîne. À la fin de la chaîne de fonctions, vous vous retrouvez généralement avec un Sequencefichier. Ensuite , vous pouvez utiliser toList(), toSet(), toMap()ou une autre fonction de matérialiser la Sequencefin.

// switch to and from lazy
val someList = items.asSequence().filter { ... }.take(10).map { ... }.toList()

// switch to lazy, but sorted() brings us out again at the end
val someList = items.asSequence().filter { ... }.take(10).map { ... }.sorted()

Pourquoi n'y a-t-il pas de types?!?

Vous remarquerez que les exemples Kotlin ne spécifient pas les types. En effet, Kotlin a une inférence de type complète et est complètement sûr au moment de la compilation. Plus que Java, car il a également des types nullables et peut aider à prévenir le redoutable NPE. Donc ceci à Kotlin:

val someList = people.filter { it.age <= 30 }.map { it.name }

est le même que:

val someList: List<String> = people.filter { it.age <= 30 }.map { it.name }

Parce que Kotlin sait ce que peoplec'est, et que people.agec'est Intdonc l'expression de filtre ne permet que la comparaison à un Int, et c'est people.nameun Stringdonc l' mapétape produit un List<String>(lecture seule Listde String).

Maintenant, si peoplec'était possible null, comme dans un List<People>?alors:

val someList = people?.filter { it.age <= 30 }?.map { it.name }

Renvoie un List<String>?qui devrait être vérifié par null ( ou utilisez l'un des autres opérateurs Kotlin pour les valeurs Nullable, voir cette manière idiomatique de Kotlin de traiter les valeurs Nullables et également la manière idiomatique de gérer une liste Nullable ou vide dans Kotlin )

Voir également:

Jayson Minard
la source
Existe-t-il un équivalent à parallelStream () de Java8 dans Kotlin?
arnab
La réponse sur les collections immuables et Kotlin est la même réponse pour @arnab ici pour le parallèle, d'autres bibliothèques existent, utilisez-les: stackoverflow.com/a/34476880/3679676
Jayson Minard
2
@arnab Vous voudrez peut-être consulter le support Kotlin pour les fonctionnalités Java 7/8 (en particulier, kotlinx-support-jdk8) qui a été rendu disponible plus tôt cette année: discuss.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
roborative
Est-il vraiment idiomatique d'utiliser 3 références «it» différentes dans une seule déclaration?
herman
2
C'est une préférence, dans les exemples ci-dessus, je les gardais courts et ne fournissais un nom local pour un paramètre que si nécessaire.
Jayson Minard
47

Pour des exemples supplémentaires, voici tous les exemples de Java 8 Stream Tutorial convertis en Kotlin. Le titre de chaque exemple est dérivé de l'article source:

Comment fonctionnent les flux

// Java:
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList.stream()
      .filter(s -> s.startsWith("c"))
      .map(String::toUpperCase)
     .sorted()
     .forEach(System.out::println);

// C1
// C2
// Kotlin:
val list = listOf("a1", "a2", "b1", "c2", "c1")
list.filter { it.startsWith('c') }.map (String::toUpperCase).sorted()
        .forEach (::println)

Différents types de flux # 1

// Java:
Arrays.asList("a1", "a2", "a3")
    .stream()
    .findFirst()
    .ifPresent(System.out::println);    
// Kotlin:
listOf("a1", "a2", "a3").firstOrNull()?.apply(::println)

ou, créez une fonction d'extension sur String appelée ifPresent:

// Kotlin:
inline fun String?.ifPresent(thenDo: (String)->Unit) = this?.apply { thenDo(this) }

// now use the new extension function:
listOf("a1", "a2", "a3").firstOrNull().ifPresent(::println)

Voir aussi: apply()fonction

Voir aussi: Fonctions d'extension

Voir aussi: ?.Opérateur Safe Call , et en général nullabilité: dans Kotlin, quelle est la manière idiomatique de traiter les valeurs nullables, de les référencer ou de les convertir

Différents types de flux # 2

// Java:
Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);    
// Kotlin:
sequenceOf("a1", "a2", "a3").firstOrNull()?.apply(::println)

Différents types de flux # 3

// Java:
IntStream.range(1, 4).forEach(System.out::println);
// Kotlin:  (inclusive range)
(1..3).forEach(::println)

Différents types de flux # 4

// Java:
Arrays.stream(new int[] {1, 2, 3})
    .map(n -> 2 * n + 1)
    .average()
    .ifPresent(System.out::println); // 5.0    
// Kotlin:
arrayOf(1,2,3).map { 2 * it + 1}.average().apply(::println)

Différents types de flux # 5

// Java:
Stream.of("a1", "a2", "a3")
    .map(s -> s.substring(1))
    .mapToInt(Integer::parseInt)
    .max()
    .ifPresent(System.out::println);  // 3
// Kotlin:
sequenceOf("a1", "a2", "a3")
    .map { it.substring(1) }
    .map(String::toInt)
    .max().apply(::println)

Différents types de flux # 6

// Java:
IntStream.range(1, 4)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);

// a1
// a2
// a3    
// Kotlin:  (inclusive range)
(1..3).map { "a$it" }.forEach(::println)

Différents types de flux # 7

// Java:
Stream.of(1.0, 2.0, 3.0)
    .mapToInt(Double::intValue)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);

// a1
// a2
// a3
// Kotlin:
sequenceOf(1.0, 2.0, 3.0).map(Double::toInt).map { "a$it" }.forEach(::println)

Pourquoi la commande est importante

Cette section du didacticiel Java 8 Stream est la même pour Kotlin et Java.

Réutilisation des flux

Dans Kotlin, cela dépend du type de collection si elle peut être consommée plus d'une fois. A Sequencegénère un nouvel itérateur à chaque fois, et à moins qu'il n'affirme "utiliser une seule fois", il peut se réinitialiser au début chaque fois qu'il est actionné. Par conséquent, alors que ce qui suit échoue dans le flux Java 8, mais fonctionne dans Kotlin:

// Java:
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("b"));

stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception
// Kotlin:  
val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }

stream.forEach(::println) // b1, b2

println("Any B ${stream.any { it.startsWith('b') }}") // Any B true
println("Any C ${stream.any { it.startsWith('c') }}") // Any C false

stream.forEach(::println) // b1, b2

Et en Java pour obtenir le même comportement:

// Java:
Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
          .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

Par conséquent, dans Kotlin, le fournisseur des données décide s'il peut se réinitialiser et fournir un nouvel itérateur ou non. Mais si vous souhaitez contraindre intentionnellement une Sequenceitération à une fois, vous pouvez utiliser la constrainOnce()fonction Sequencecomme suit:

val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }
        .constrainOnce()

stream.forEach(::println) // b1, b2
stream.forEach(::println) // Error:java.lang.IllegalStateException: This sequence can be consumed only once. 

Opérations avancées

Collectez l'exemple n ° 5 (oui, j'ai sauté ceux déjà dans l'autre réponse)

// Java:
String phrase = persons
        .stream()
        .filter(p -> p.age >= 18)
        .map(p -> p.name)
        .collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));

    System.out.println(phrase);
    // In Germany Max and Peter and Pamela are of legal age.    
// Kotlin:
val phrase = persons.filter { it.age >= 18 }.map { it.name }
        .joinToString(" and ", "In Germany ", " are of legal age.")

println(phrase)
// In Germany Max and Peter and Pamela are of legal age.

Et en remarque, dans Kotlin, nous pouvons créer des classes de données simples et instancier les données de test comme suit:

// Kotlin:
// data class has equals, hashcode, toString, and copy methods automagically
data class Person(val name: String, val age: Int) 

val persons = listOf(Person("Tod", 5), Person("Max", 33), 
                     Person("Frank", 13), Person("Peter", 80),
                     Person("Pamela", 18))

Collectez l'exemple n ° 6

// Java:
Map<Integer, String> map = persons
        .stream()
        .collect(Collectors.toMap(
                p -> p.age,
                p -> p.name,
                (name1, name2) -> name1 + ";" + name2));

System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}    

Ok, un cas plus intéressant ici pour Kotlin. D'abord les mauvaises réponses pour explorer les variantes de création d'un à Mappartir d'une collection / séquence:

// Kotlin:
val map1 = persons.map { it.age to it.name }.toMap()
println(map1)
// output: {18=Max, 23=Pamela, 12=David} 
// Result: duplicates overridden, no exception similar to Java 8

val map2 = persons.toMap({ it.age }, { it.name })
println(map2)
// output: {18=Max, 23=Pamela, 12=David} 
// Result: same as above, more verbose, duplicates overridden

val map3 = persons.toMapBy { it.age }
println(map3)
// output: {18=Person(name=Max, age=18), 23=Person(name=Pamela, age=23), 12=Person(name=David, age=12)}
// Result: duplicates overridden again

val map4 = persons.groupBy { it.age }
println(map4)
// output: {18=[Person(name=Max, age=18)], 23=[Person(name=Peter, age=23), Person(name=Pamela, age=23)], 12=[Person(name=David, age=12)]}
// Result: closer, but now have a Map<Int, List<Person>> instead of Map<Int, String>

val map5 = persons.groupBy { it.age }.mapValues { it.value.map { it.name } }
println(map5)
// output: {18=[Max], 23=[Peter, Pamela], 12=[David]}
// Result: closer, but now have a Map<Int, List<String>> instead of Map<Int, String>

Et maintenant pour la bonne réponse:

// Kotlin:
val map6 = persons.groupBy { it.age }.mapValues { it.value.joinToString(";") { it.name } }

println(map6)
// output: {18=Max, 23=Peter;Pamela, 12=David}
// Result: YAY!!

Nous avions juste besoin de joindre les valeurs correspondantes pour réduire les listes et fournir un transformateur jointToStringpour passer de l' Personinstance au Person.name.

Collectez l'exemple n ° 7

Ok, celui-ci peut facilement être fait sans coutume Collector, donc résolvons-le à la manière de Kotlin, puis inventons un nouvel exemple qui montre comment faire un processus similaire pour Collector.summarizingIntlequel n'existe pas nativement dans Kotlin.

// Java:
Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
        () -> new StringJoiner(" | "),          // supplier
        (j, p) -> j.add(p.name.toUpperCase()),  // accumulator
        (j1, j2) -> j1.merge(j2),               // combiner
        StringJoiner::toString);                // finisher

String names = persons
        .stream()
        .collect(personNameCollector);

System.out.println(names);  // MAX | PETER | PAMELA | DAVID    
// Kotlin:
val names = persons.map { it.name.toUpperCase() }.joinToString(" | ")

Ce n'est pas ma faute, ils ont choisi un exemple trivial !!! Ok, voici une nouvelle summarizingIntméthode pour Kotlin et un échantillon correspondant:

Exemple SummarizingInt

// Java:
IntSummaryStatistics ageSummary =
    persons.stream()
           .collect(Collectors.summarizingInt(p -> p.age));

System.out.println(ageSummary);
// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}    
// Kotlin:

// something to hold the stats...
data class SummaryStatisticsInt(var count: Int = 0,  
                                var sum: Int = 0, 
                                var min: Int = Int.MAX_VALUE, 
                                var max: Int = Int.MIN_VALUE, 
                                var avg: Double = 0.0) {
    fun accumulate(newInt: Int): SummaryStatisticsInt {
        count++
        sum += newInt
        min = min.coerceAtMost(newInt)
        max = max.coerceAtLeast(newInt)
        avg = sum.toDouble() / count
        return this
    }
}

// Now manually doing a fold, since Stream.collect is really just a fold
val stats = persons.fold(SummaryStatisticsInt()) { stats, person -> stats.accumulate(person.age) }

println(stats)
// output: SummaryStatisticsInt(count=4, sum=76, min=12, max=23, avg=19.0)

Mais il est préférable de créer une fonction d'extension, 2 pour faire correspondre les styles dans Kotlin stdlib:

// Kotlin:
inline fun Collection<Int>.summarizingInt(): SummaryStatisticsInt
        = this.fold(SummaryStatisticsInt()) { stats, num -> stats.accumulate(num) }

inline fun <T: Any> Collection<T>.summarizingInt(transform: (T)->Int): SummaryStatisticsInt =
        this.fold(SummaryStatisticsInt()) { stats, item -> stats.accumulate(transform(item)) }

Vous avez maintenant deux façons d'utiliser les nouvelles summarizingIntfonctions:

val stats2 = persons.map { it.age }.summarizingInt()

// or

val stats3 = persons.summarizingInt { it.age }

Et tout cela produit les mêmes résultats. Nous pouvons également créer cette extension pour travailler sur Sequenceet pour les types primitifs appropriés.

Pour le plaisir, comparez le code Java JDK au code personnalisé Kotlin requis pour implémenter cette synthèse.

Jayson Minard
la source
Dans le flux 5, il n'y a aucun avantage à utiliser deux cartes au lieu d'une .map { it.substring(1).toInt() }: comme vous le savez bien, le type inféré est celui de la puissance kotlin.
Michele d'Amico
vrai, mais il n'y a pas non plus d'inconvénient (pour la comparabilité, je les ai gardés séparés)
Jayson Minard
Mais le code Java peut facilement être mis en parallèle, donc dans de nombreux cas, vous feriez mieux d'appeler le code de flux Java de Kotlin.
Howard Lovatt
@HowardLovatt il existe de nombreux cas où le parallèle n'est pas la voie à suivre, en particulier dans les environnements simultanés lourds où vous êtes déjà dans un pool de threads. Je parie que le cas d'utilisation moyen n'est PAS parallèle, et c'est le cas rare. Mais bien sûr, vous avez toujours la possibilité d'utiliser les classes Java comme bon vous semble, et rien de tout cela n'était vraiment le but de cette question et réponse.
Jayson Minard
3

Il y a des cas où il est difficile d'éviter d'appeler collect(Collectors.toList())ou similaire. Dans ces cas, vous pouvez passer plus rapidement à un équivalent Kotlin en utilisant des fonctions d'extension telles que:

fun <T: Any> Stream<T>.toList(): List<T> = this.collect(Collectors.toList<T>())
fun <T: Any> Stream<T>.asSequence(): Sequence<T> = this.iterator().asSequence()

Ensuite, vous pouvez simplement stream.toList()ou stream.asSequence()revenir à l'API Kotlin. Un cas tel que Files.list(path)vous oblige à Streamne pas le vouloir, et ces extensions peuvent vous aider à revenir aux collections standard et à l'API Kotlin.

Jayson Minard
la source
2

En savoir plus sur la paresse

Prenons l'exemple de solution pour "Calculer la somme des salaires par département" donnée par Jayson:

val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}

Afin de rendre cela paresseux (c'est-à-dire éviter de créer une carte intermédiaire dans l' groupByétape), il n'est pas possible d'utiliser asSequence(). Au lieu de cela, nous devons utiliser groupingByet foldopération:

val totalByDept = employees.groupingBy { it.dept }.fold(0) { acc, e -> acc + e.salary }

Pour certaines personnes, cela peut même être plus lisible, puisque vous n'avez pas affaire à des entrées de carte: la it.valuepartie de la solution était déroutante pour moi aussi au début.

Comme il s'agit d'un cas courant et que nous préférerions ne pas l'écrire à foldchaque fois, il peut être préférable de simplement fournir une sumByfonction générique sur Grouping:

public inline fun <T, K> Grouping<T, K>.sumBy(
        selector: (T) -> Int
): Map<K, Int> = 
        fold(0) { acc, element -> acc + selector(element) }

afin que nous puissions simplement écrire:

val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }
Herman
la source