Quali equivalenti Java 8 Stream.collect sono disponibili nella libreria Kotlin standard?


181

In Java 8, c'è ciò Stream.collectche consente le aggregazioni sulle raccolte. In Kotlin, questo non esiste allo stesso modo, tranne forse come una raccolta di funzioni di estensione nello stdlib. Ma non è chiaro quali siano le equivalenze per i diversi casi d'uso.

Ad esempio, nella parte superiore di JavaDoc ciCollectors sono esempi scritti per Java 8 e quando si esegue il porting su Kolin non è possibile utilizzare le classi Java 8 su una versione JDK diversa, quindi è probabile che debbano essere scritti in modo diverso.

In termini di risorse online che mostrano esempi di collezioni Kotlin, sono in genere banali e non si confrontano realmente con gli stessi casi d'uso. Quali sono buoni esempi che corrispondono davvero ai casi documentati per Java 8 Stream.collect? L'elenco è lì:

  • Accumula nomi in un elenco
  • Accumula nomi in un TreeSet
  • Converti gli elementi in stringhe e concatenali, separati da virgole
  • Calcola la somma degli stipendi del dipendente
  • Dipendenti del gruppo per dipartimento
  • Calcola la somma degli stipendi per dipartimento
  • Suddividere gli studenti nel passaggio e nel fallimento

Con i dettagli in JavaDoc collegati sopra.

Nota: questa domanda è stata scritta e risposta intenzionalmente dall'autore ( domande con risposta autonoma ), in modo che le risposte idiomatiche agli argomenti di Kotlin più comuni siano presenti in SO. Anche per chiarire alcune risposte molto vecchie scritte per gli alfa di Kotlin che non sono accurate per l'attuale Kotlin.


Nei casi in cui non hai altra scelta che utilizzare collect(Collectors.toList())o simili, potresti riscontrare questo problema: stackoverflow.com/a/35722167/3679676 (il problema, con soluzioni alternative)
Jayson Minard

Risposte:


257

Ci sono funzioni nello stdlib di Kotlin per media, conteggio, distinto, filtraggio, ricerca, raggruppamento, unione, mappatura, min, max, partizionamento, suddivisione, ordinamento, somma, da / verso array, da / per elenchi, da / per mappe , unione, coiterazione, tutti i paradigmi funzionali e altro ancora. Quindi puoi usarli per creare piccoli 1-liner e non è necessario utilizzare la sintassi più complicata di Java 8.

Penso che l'unica cosa che manca alla Collectorsclasse Java 8 integrata sia la sintesi (ma in un'altra risposta a questa domanda è una soluzione semplice) .

Una cosa che manca a entrambi è il raggruppamento per conteggio, che è visto in un'altra risposta Stack Overflow e ha anche una risposta semplice. Un altro caso interessante è anche quello di StackTranslate.it: modo idiomatico di riversare la sequenza in tre liste usando Kotlin . E se vuoi creare qualcosa di simile Stream.collectper un altro scopo, vedi Stream.collect personalizzato in Kotlin

EDIT 11.08.2017: Le operazioni di raccolta in blocchi / finestre sono state aggiunte in kotlin 1.2 M2, vedere https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


È sempre utile esplorare il riferimento API per kotlin.collections nel suo insieme prima di creare nuove funzioni che potrebbero già esistere lì.

Ecco alcune conversioni dagli Stream.collectesempi Java 8 all'equivalente in Kotlin:

Accumula nomi in un elenco

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

Converti gli elementi in stringhe e concatenali, separati da virgole

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

Calcola la somma degli stipendi del dipendente

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

Dipendenti del gruppo per dipartimento

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

Calcola la somma degli stipendi per dipartimento

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

Suddividere gli studenti nel passaggio e nel fallimento

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

Nomi dei membri maschi

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

Raggruppa i nomi dei membri nell'elenco per sesso

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

Filtra un elenco in un altro elenco

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

Trovare la stringa più breve un elenco

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

Conteggio degli elementi in un elenco dopo l'applicazione del filtro

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

e continua ... In tutti i casi, non è richiesta alcuna piega speciale, riduzione o altra funzionalità per imitare Stream.collect. Se hai ulteriori casi d'uso, aggiungili nei commenti e possiamo vedere!

A proposito di pigrizia

Se si desidera elaborare una catena in modo pigro, è possibile convertirla in un Sequenceutilizzo asSequence()prima della catena. Alla fine della catena di funzioni, di solito si finisce anche con un Sequence. Quindi è possibile utilizzare toList(), toSet(), toMap()o qualche altra funzione dare contenuti concreti alla Sequencefine.

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

Perché non ci sono tipi?!?

Noterai che gli esempi di Kotlin non specificano i tipi. Questo perché Kotlin ha un'inferenza del tipo completo ed è completamente sicuro al momento della compilazione. Più di Java perché ha anche tipi nullable e può aiutare a prevenire l'NPE temuto. Quindi questo a Kotlin:

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

equivale a:

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

Poiché Kotlin sa cosa peopleè, e che people.ageè Intquindi espressione filtro consente solo rispetto ad una Int, e che people.nameè Stringquindi la mapfase produce una List<String>(sola lettura Listdi String).

Ora, se peoplefosse possibile null, come-in a List<People>?allora:

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

Restituisce un valore List<String>?che dovrebbe essere null controllato ( o utilizzare uno degli altri operatori Kotlin per valori nullable, vedere questo modo idiomatico di Kotlin per gestire i valori nullable e anche Modo idiomatico di gestire l'elenco nullable o vuoto in Kotlin )

Guarda anche:


Esiste un equivalente al parallelStream () di Java8 in Kotlin?
Arnab,

La risposta sulle collezioni immutabili e Kotlin è la stessa risposta per @arnab qui per parallelo, esistono altre biblioteche, usarli: stackoverflow.com/a/34476880/3679676
Jayson Minard

2
@arnab Potresti voler consultare il supporto Kotlin per le funzionalità Java 7/8 (in particolare, kotlinx-support-jdk8) che è stato reso disponibile all'inizio di quest'anno: discuss.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
roborativo

È davvero idiomatico usare 3 diversi riferimenti "it" in una frase?
herman,

2
È una preferenza, negli esempi sopra li stavo mantenendo brevi e fornendo solo un nome locale per un parametro se necessario.
Jayson Minard,

47

Per ulteriori esempi, ecco tutti gli esempi di Java 8 Stream Tutorial convertiti in Kotlin. Il titolo di ciascun esempio deriva dall'articolo di origine:

Come funzionano i flussi

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

Diversi tipi di stream # 1

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

oppure, crea una funzione di estensione su String chiamata 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)

Vedi anche: apply()funzione

Vedi anche: Funzioni di estensione

Vedi anche: ?.Operatore Chiamata sicura , e in generale nullabilità: in Kotlin, qual è il modo idiomatico per gestire valori nullable, fare riferimento o convertirli

Diversi tipi di stream # 2

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

Diversi tipi di stream # 3

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

Diversi tipi di stream # 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)

Diversi tipi di stream # 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)

Diversi tipi di stream # 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)

Diversi tipi di flussi # 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)

Perché l'ordine conta

Questa sezione del Java 8 Stream Tutorial è la stessa per Kotlin e Java.

Riutilizzo degli stream

In Kotlin, dipende dal tipo di raccolta se può essere consumato più di una volta. A Sequencegenera un nuovo iteratore ogni volta e, a meno che non asserisca "usa solo una volta", può reimpostare all'inizio ogni volta che viene attivato. Pertanto, mentre quanto segue non riesce nel flusso Java 8, ma funziona in 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

E in Java per ottenere lo stesso comportamento:

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

Pertanto in Kotlin il fornitore dei dati decide se può ripristinare e fornire un nuovo iteratore o meno. Ma se si desidera limitare intenzionalmente una Sequenceiterazione a una volta, è possibile utilizzare la constrainOnce()funzione Sequencecome segue:

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. 

Operazioni avanzate

Raccogli l'esempio n. 5 (sì, ho saltato quelli già nell'altra risposta)

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

E come nota a margine, in Kotlin possiamo creare semplici classi di dati e creare un'istanza dei dati di test come segue:

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

Raccogli l'esempio # 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 caso più interessante qui per Kotlin. Innanzitutto le risposte sbagliate per esplorare le varianti della creazione di una Mapda una raccolta / sequenza:

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

E ora per la risposta corretta:

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

Dovevamo solo unire i valori corrispondenti per comprimere gli elenchi e fornire un trasformatore jointToStringper passare Persondall'istanza a Person.name.

Raccogli l'esempio n. 7

Ok, questo può essere fatto facilmente senza un'usanza Collector, quindi risolviamolo in modo Kotlin, quindi inventiamo un nuovo esempio che mostra come eseguire un processo simile per il Collector.summarizingIntquale non esiste nativamente in 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(" | ")

Non è colpa mia se hanno scelto un esempio banale !!! Ok, ecco un nuovo summarizingIntmetodo per Kotlin e un campione corrispondente:

Esempio di riepilogo

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

Ma è meglio creare una funzione di estensione, 2 in realtà per abbinare gli stili in 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)) }

Ora hai due modi per usare le nuove summarizingIntfunzioni:

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

// or

val stats3 = persons.summarizingInt { it.age }

E tutti questi producono gli stessi risultati. Possiamo anche creare questa estensione su cui lavorareSequence e per i tipi primitivi appropriati.

Per divertimento, confronta il codice JDK Java con il codice personalizzato Kotlin richiesto per implementare questo riepilogo.


Nello stream 5 non c'è alcun vantaggio nell'usare due mappe invece di una .map { it.substring(1).toInt() }: come sai il tipo ben dedotto è uno dei poteri di kotlin.
Michele d'Amico,

vero, ma non c'è neanche un aspetto negativo (per comparabilità li ho tenuti separati)
Jayson Minard

Ma il codice Java può essere facilmente reso parallelo, quindi in molti casi sarebbe meglio chiamare il codice stream Java da Kotlin.
Howard Lovatt,

@HowardLovatt ci sono molti casi in cui il parallelo non è la strada da percorrere, specialmente in ambienti concomitanti pesanti in cui ci si trova già in un pool di thread. Scommetto che il caso d'uso medio NON è parallelo, ed è il caso raro. Ma, naturalmente, hai sempre la possibilità di utilizzare le classi Java come ritieni opportuno, e nulla di tutto questo era davvero lo scopo di questa domanda e risposta.
Jayson Minard,

3

Ci sono alcuni casi in cui è difficile evitare di chiamare collect(Collectors.toList())o simili. In questi casi, puoi passare più rapidamente a un equivalente di Kotlin usando funzioni di estensione come:

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

Quindi puoi semplicemente stream.toList()ostream.asSequence() tornare all'API di Kotlin. Un caso come quello Files.list(path)ti costringe a un Streamquando potresti non volerlo, e queste estensioni possono aiutarti a tornare alle raccolte standard e all'API di Kotlin.


2

Altro sulla pigrizia

Prendiamo la soluzione di esempio per "Calcola la somma degli stipendi per dipartimento" fornita da Jayson:

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

Per renderlo pigro (cioè evitare di creare una mappa intermedia nel groupBypassaggio), non è possibile utilizzare asSequence(). Invece, dobbiamo usare groupingBye foldoperare:

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

Per alcune persone questo potrebbe anche essere più leggibile, dal momento che non hai a che fare con le voci della mappa: la it.valueparte nella soluzione era inizialmente confusa anche per me.

Dato che questo è un caso comune e preferiremmo non scriverlo foldogni volta, potrebbe essere meglio fornire una sumByfunzione generica su Grouping:

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

in modo che possiamo semplicemente scrivere:

val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.