220 votes

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

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

Par exemple, au en haut de la JavaDoc pour Collectors sont des exemples écrits pour Java 8, et lors de leur portage vers Kolin, vous ne pouvez pas utiliser les classes Java 8 sur une version différente du JDK, il est donc probable qu'ils doivent être écrits différemment.

En termes de ressources en ligne montrant des exemples de collections Kotlin, ils sont généralement triviaux 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 est la suivante :

  • Accumuler les noms dans une liste
  • Accumuler les noms dans un TreeSet
  • Convertir les éléments en chaînes de caractères et les concaténer, séparés par des virgules.
  • Calculer la somme des salaires de l'employé
  • Employés du groupe par département
  • Calculer la somme des salaires par département
  • Répartir les élèves entre ceux qui réussissent et ceux qui échouent

Les détails se trouvent dans le JavaDoc lié ci-dessus.

Note : cette question est intentionnellement écrite et répondue par l'auteur ( Questions auto-répondues ), de sorte que les réponses idiomatiques aux questions les plus courantes sur Kotlin soient présentes dans SO. Il s'agit également de clarifier certaines réponses très anciennes écrites pour les alphas de Kotlin qui ne sont pas exactes pour le Kotlin actuel.

0 votes

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

310voto

Jayson Minard Points 5925

Il existe des fonctions dans la bibliothèque standard Kotlin pour les fonctions suivantes : average, count, distinct, filtering, finding, grouping, joining, mapping, min, max, partitioning, slicing, sorting, summing, to/from arrays, to/from lists, to/from maps, union, co-iteration, tous les paradigmes fonctionnels, etc. 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 à la version intégrée de Java 8 Collectors est le résumé (mais en une autre réponse à cette question est une solution simple) .

Une chose qui manque dans les deux cas est la mise en lot par comptage, que l'on voit dans une autre réponse de Stack Overflow et a également une réponse simple. Un autre cas intéressant est celui-là, également tiré de Stack Overflow : Méthode idiomatique pour diviser une séquence en trois listes à l'aide de Kotlin . Et si vous voulez créer quelque chose comme Stream.collect dans un autre but, voir Stream.collect personnalisé en Kotlin

MODIFIER 11.08.2017 : Les opérations de collecte de type "chunked/windowed" ont été ajoutées dans la version M2 de kotlin 1.2, cf. https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


Il est toujours bon d'explorer les 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 de Java 8 Stream.collect à l'équivalent en 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 de caractères 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 }

Employés du groupe 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 }}

Répartir les élèves entre ceux qui réussissent et ceux qui échouent

// 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 du fichier 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') } 

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 }

Compter les éléments d'une liste après l'application d'un 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, aucun pliage, réduction ou autre fonctionnalité spéciale 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 une chaîne paresseusement, vous pouvez la convertir en un fichier de type Sequence en utilisant asSequence() avant la chaîne. À la fin de la chaîne de fonctions, on se retrouve généralement avec une fonction Sequence également. Vous pouvez alors utiliser toList() , toSet() , toMap() ou une autre fonction pour matérialiser le 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 précisent pas les types. C'est parce que Kotlin a une inférence de type complète et est complètement sûr au moment de la compilation. C'est encore mieux que Java, car il dispose également de types nullables, ce qui permet d'éviter le redoutable NPE. Donc, ceci en Kotlin :

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

est la même chose que :

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

Parce que Kotlin sait ce que people est, et que people.age es Int par conséquent, l'expression du filtre ne permet que la comparaison avec un Int et que people.name est un String par conséquent, le map l'étape produit un List<String> (en lecture seule List de String ).

Maintenant, si people étaient peut-être null comme dans un List<People>? alors :

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

Retourne un List<String>? qui devraient être contrôlés par la méthode de la nullité ( ou utiliser l'un des autres opérateurs Kotlin pour les valeurs nulles, voir ceci Manière idiomatique en Kotlin de traiter les valeurs nullables et aussi Manière idiomatique de traiter les listes nullables ou vides en Kotlin )

Voir aussi :

0 votes

Existe-t-il un équivalent de parallelStream() de Java8 en Kotlin ?

0 votes

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

2 votes

@arnab Vous pouvez consulter le support Kotlin pour Java 7/8 (en particulier, kotlinx-support-jdk8) qui a été mis à disposition au début de cette année : discuss.kotlinlang.org/t/jdk7-8-features-in-kotlin-1-0/1625

50voto

Jayson Minard Points 5925

Pour des exemples supplémentaires, voici tous les échantillons de Tutoriel sur les flux de Java 8 converti 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 cours d'eau #1

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

// Kotlin:
listOf("a1", "a2", "a3").firstOrNull()?.apply(::println)

ou, créer 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, la nullité : En Kotlin, quelle est la façon idiomatique de traiter les valeurs nulles, en les référençant ou en les convertissant

Différents types de cours d'eau #2

// Java:
Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);    

// Kotlin:
sequenceOf("a1", "a2", "a3").firstOrNull()?.apply(::println)

Différents types de cours d'eau #3

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

// Kotlin:  (inclusive range)
(1..3).forEach(::println)

Différents types de cours d'eau #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 cours d'eau #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 cours d'eau #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 cours d'eau #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 sur les flux de Java 8 est la même pour Kotlin et Java.

Réutilisation des flux

En Kotlin, il dépend du type de collection si elle peut être consommée plus d'une fois. A Sequence génère un nouvel itérateur à chaque fois, et à moins qu'il n'affirme "use only once", il peut revenir au début à chaque fois qu'il est utilisé. Par conséquent, alors que ce qui suit échoue dans le flux Java 8, il 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, en Kotlin, le fournisseur de données décide s'il peut revenir en arrière et fournir un nouvel itérateur ou non. Mais si vous voulez contraindre intentionnellement un Sequence à une seule itération, vous pouvez utiliser constrainOnce() fonction 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

Exemple de collecte #5 (oui, j'ai déjà sauté ceux-là 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 pour l'anecdote, en Kotlin, nous pouvons créer de simples classes de données et instanciez 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))

Exemple de collecte #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 variations de la création d'un 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, 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!!

Il nous suffisait de joindre les valeurs correspondantes pour réduire les listes et fournir un transformateur à l'adresse suivante jointToString pour passer de Person à l'instance Person.name .

Collecte de l'exemple n° 7

Ok, celui-ci peut facilement être fait sans une coutume Collector Nous allons donc résoudre ce problème à la manière de Kotlin, puis créer un nouvel exemple qui montre comment procéder de manière similaire pour Collector.summarizingInt qui 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 s'ils ont choisi un exemple insignifiant ! !! Ok, voici un nouveau summarizingInt pour Kotlin et un échantillon correspondant :

Exemple de 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 en fait 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 le nouveau summarizingInt fonctions :

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

// or

val stats3 = persons.summarizingInt { it.age }

Et tous ces éléments produisent les mêmes résultats. Nous pouvons également créer cette extension pour travailler sur Sequence et pour les types primitifs appropriés.

Pour s'amuser, comparer le code Java JDK et le code personnalisé Kotlin nécessaire pour mettre en œuvre cette compression.

0 votes

Dans le flux 5, il n'y a aucun avantage à utiliser deux cartes au lieu d'une seule. .map { it.substring(1).toInt() } Comme vous le savez bien, le type inféré est l'un des pouvoirs de Kotlin.

0 votes

C'est vrai, mais il n'y a pas d'inconvénient non plus (pour des raisons de comparabilité, je les ai gardés séparés)

0 votes

Mais le code Java peut facilement être mis en parallèle, de sorte que dans de nombreux cas, il est préférable d'appeler le code du flux Java à partir de Kotlin.

3voto

Jayson Minard Points 5925

Il y a des cas où il est difficile d'éviter d'appeler collect(Collectors.toList()) ou similaire. Dans ces cas, vous pouvez plus rapidement passer à 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()

Vous pouvez alors simplement stream.toList() o stream.asSequence() pour revenir dans l'API Kotlin. Un cas tel que Files.list(path) vous contraint à un Stream lorsque vous ne le souhaitez pas, et ces extensions peuvent vous aider à revenir aux collections standard et à l'API Kotlin.

2voto

herman Points 1358

Plus sur la paresse

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

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

Afin de rendre cette opération paresseuse (c'est-à-dire d'éviter de créer une carte intermédiaire dans le fichier groupBy ), il n'est pas possible d'utiliser asSequence() . Au lieu de cela, nous devons utiliser groupingBy y fold fonctionnement :

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

Pour certaines personnes, cela peut même être plus lisible, car il ne s'agit pas d'entrées de carte : l'élément it.value dans la solution m'a aussi dérouté au début.

Comme il s'agit d'un cas courant et que nous préférons ne pas avoir à écrire l'adresse de la fold à chaque fois, il peut être préférable de fournir un fichier générique sumBy fonction 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 }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X