web-dev-qa-db-fra.com

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

Dans Java 8, il existe Stream.collect qui permet les agrégations sur des collections. En Kotlin, cela n’existe pas de la même manière, si ce n’est peut-être sous forme de collection de fonctions d’extension dans la bibliothèque stdlib. Mais il n'est pas clair quelles sont les équivalences pour différents cas d'utilisation.

Par exemple, en haut de JavaDoc pour Collectors , des exemples sont écrits pour Java 8 et vous ne pouvez pas utiliser le Java 8 classes sur une autre version du JDK, elles devraient donc probablement être écrites 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 documentés pour Java 8 _Stream.collect_? La liste est la suivante:

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

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

Remarque: cette question est intentionnellement écrite et répondue par l'auteur ( Self-Answered Questions ), afin que les réponses idiomatiques aux sujets de Kotlin fréquemment demandés soient présentes dans SO. Également pour clarifier certaines très vieilles réponses écrites pour les alphas de Kotlin qui ne sont pas exactes pour le Kotlin actuel.

160
Jayson Minard

Il existe des fonctions dans Kotlin stdlib pour moyenne, nombre, distinct, filtrage, recherche, regroupement, jonction, mappage, min, max, partitionnement, découpage, tri, sommation, vers/à partir de tableaux, vers/depuis des listes, vers/à partir de cartes , union, co-itération, tous les paradigmes fonctionnels, et plus. Vous pouvez donc utiliser ceux-ci pour créer de petites lignes 1-l 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 à la classe Java 8 Collectors intégrée est la synthèse (mais dans ne autre réponse à cette question est une solution simple) .

Une des choses qui manque dans les deux cas est le lot par nombre, qui est vu dans ne autre réponse de Stack Overflow et a également une réponse simple. Un autre cas intéressant est celui également de Stack Overflow: moyen idiomatique de renverser la séquence en trois listes en utilisant Kotlin . Et si vous voulez créer quelque chose comme Stream.collect dans un autre but, voir Custom Stream.collect in Kotlin

EDIT 11.08.2017: Des opérations de collecte par morceaux/par 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 l’ensemble référence API pour kotlin.collections avant de créer de nouvelles fonctions qui pourraient déjà exister là-bas.

Voici quelques conversions d'exemples de Java 8 Stream.collect en équivalents dans Kotlin:

Accumuler des 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

Convertissez les éléments en chaînes et concatérez-les, séparés par des virgules

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

Calcule la somme des salaires de l'employé

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

Groupe d'employés par département

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

Calcule 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 }}

Répartir les étudiants en réussite 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 }

Noms de groupe 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 dans 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') } 

Trouver 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 d'une liste après 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 ça continue ... Dans tous les cas, aucun pli spécial, réduction ou autre fonctionnalité n'était nécessaire pour imiter Stream.collect. Si vous avez d'autres cas d'utilisation, ajoutez-les dans les commentaires et nous pourrons voir!

A propos de la paresse

Si vous souhaitez traiter paresseux une chaîne, vous pouvez convertir en Sequence en utilisant asSequence() avant la chaîne. À la fin de la chaîne de fonctions, vous vous retrouvez généralement avec un Sequence. Vous pouvez ensuite utiliser toList(), toSet(), toMap() ou une autre fonction pour matérialiser la Sequence à la fin.

// 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. Cela est dû au fait que Kotlin a une inférence de type complète et qu'il est totalement sans danger pour le type au moment de la compilation. Plus que Java, car il possède également des types nullable et peut aider à empêcher le NPE redouté. 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 qu'est people, et que people.age est Int, l'expression du filtre permet uniquement la comparaison avec un Int, et que people.name est un String donc l'étape map produit un List<String> (en lecture seule List sur String).

Maintenant, si people était éventuellement null, comme un List<People>? alors:

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

Retourne un List<String>? qui aurait besoin d'être vérifié de manière nulle ( ou d'utiliser l'un des autres opérateurs Kotlin pour les valeurs nullables, voir this manière idiomatique de Kotlin de traiter les valeurs Nullables = et aussi Maniement idiomatique de la liste nullable ou vide dans Kotlin)

Voir également:

230
Jayson Minard

Pour des exemples supplémentaires, voici tous les exemples de Tutoriel Java 8 Stream 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 une chaîne 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() function

Voir aussi: Fonctions d'extension

Voir aussi: ?. Opérateur Safe Call , et en général nullability: En Kotlin, quel est le moyen idiomatique de gérer 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 l'ordre est important

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

Réutilisation des flux

Chez Kotlin, le type de collection à utiliser peut être consommé plus d’une fois. Un Sequence génère un nouvel itérateur à chaque fois et, à moins d'affirmer "utiliser une seule fois", il peut être réinitialisé au début à chaque fois qu'il est activé. 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 dans 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

C'est pourquoi, dans Kotlin, le fournisseur de données décide s'il peut ou non réinitialiser et fournir un nouvel itérateur. Mais si vous voulez contraindre intentionnellement une itération Sequence à une fois, vous pouvez utiliser la fonction constrainOnce() pour Sequence comme 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

Recueillez l'exemple n ° 5 (oui, j'ai omis ceux déjà mentionnés 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 guise de remarque, dans Kotlin, nous pouvons créer des classes de données simples et instancier les données de test de la manière suivante:

// 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))

Recueillez l'exemple # 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 la création d'une Map à partir 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 réponse correcte:

// 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 simplement besoin de joindre les valeurs correspondantes pour réduire les listes et fournir un transformateur à jointToString pour passer de l'instance Person au Person.name.

Recueillez l'exemple n ° 7

Ok, celui-ci peut facilement être fait sans une coutume Collector, résolvons-le donc à la manière de Kotlin, puis construisons un nouvel exemple qui montre comment faire un processus similaire pour Collector.summarizingInt qui n'existe pas de manière native 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 de ma faute si ils ont choisi un exemple trivial !!! Ok, voici une nouvelle méthode summarizingInt pour Kotlin et un exemple de correspondance:

Exemple de synthèse

// 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 fonctions summarizingInt:

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 Sequence et pour les types primitifs appropriés.

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

43
Jayson Minard

Dans certains cas, il est difficile d'éviter d'appeler collect(Collectors.toList()) ou similaire. Dans ces cas, vous pouvez passer plus rapidement à un équivalent Kotlin à l'aide de 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() pour revenir à l'API Kotlin. Un cas tel que Files.list(path) vous oblige à entrer Stream lorsque vous ne le souhaitez pas et ces extensions peuvent vous aider à revenir aux collections standard et à l'API Kotlin.

3
Jayson Minard

Plus sur la paresse

Prenons l'exemple de la solution "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’étape groupBy), il n’est pas possible d’utiliser asSequence(). Au lieu de cela, nous devons utiliser les opérations groupingBy et fold:

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 partie it.value de la solution m'a aussi troublé au début.

Comme il s'agit d'un cas courant et que nous préférerions ne pas écrire la fold à chaque fois, il peut être préférable de simplement fournir une fonction sumBy 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 }
2
herman