¿Qué equivalentes Java 8 Stream.collect están disponibles en la biblioteca estándar de Kotlin?

181

En Java 8, existe el Stream.collectque permite agregaciones en colecciones. En Kotlin, esto no existe de la misma manera, aparte de quizás como una colección de funciones de extensión en stdlib. Pero no está claro cuáles son las equivalencias para diferentes casos de uso.

Por ejemplo, en la parte superior de JavaDocCollectors hay ejemplos escritos para Java 8, y cuando los transfiere a Kolin no puede usar las clases de Java 8 cuando está en una versión JDK diferente, por lo que es probable que se escriban de manera diferente.

En términos de recursos en línea que muestran ejemplos de colecciones de Kotlin, generalmente son triviales y realmente no se comparan con los mismos casos de uso. ¿Cuáles son buenos ejemplos que realmente coinciden con los casos documentados para Java 8 Stream.collect? La lista hay:

  • Acumula nombres en una lista
  • Acumula nombres en un TreeSet
  • Convierta elementos en cadenas y concatenelos, separados por comas
  • Calcular la suma de los salarios del empleado
  • Agrupar empleados por departamento
  • Calcular la suma de salarios por departamento
  • Particionar a los estudiantes para aprobar y reprobar

Con detalles en el JavaDoc vinculado anteriormente.

Nota: esta pregunta está escrita y respondida intencionalmente por el autor ( Preguntas con respuesta propia ), de modo que las respuestas idiomáticas a los temas de Kotlin más frecuentes están presentes en SO. También para aclarar algunas respuestas realmente antiguas escritas para alfas de Kotlin que no son precisas para el Kotlin actual.

Jayson Minard
fuente
En los casos en que no tenga más remedio que usar collect(Collectors.toList())o similar, puede encontrar este problema: stackoverflow.com/a/35722167/3679676 (el problema, con soluciones)
Jayson Minard

Respuestas:

257

Hay funciones en Kotlin stdlib para promediar, contar, distinguir, filtrar, buscar, agrupar, unir, mapear, min, max, particionamiento, segmentación, clasificación, suma, a / desde matrices, a / desde listas, a / desde mapas , unión, co-iteración, todos los paradigmas funcionales y más. Entonces puede usarlos para crear pequeños 1-liners y no hay necesidad de usar la sintaxis más complicada de Java 8.

Creo que lo único que falta en la Collectorsclase incorporada de Java 8 es el resumen (pero en otra respuesta a esta pregunta es una solución simple) .

Una cosa que falta en ambos es la agrupación por conteo, que se ve en otra respuesta de desbordamiento de pila y también tiene una respuesta simple. Otro caso interesante es este también de Stack Overflow: forma idiomática de secuenciar derramada en tres listas usando Kotlin . Y si desea crear algo como Stream.collectpara otro propósito, vea Custom Stream.collect en Kotlin

EDITAR 11.08.2017: Se agregaron operaciones de recolección fragmentadas / en ventanas en kotlin 1.2 M2, consulte https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


Siempre es bueno explorar la Referencia API para kotlin.collections en su conjunto antes de crear nuevas funciones que ya puedan existir allí.

Aquí hay algunas conversiones de Stream.collectejemplos de Java 8 al equivalente en Kotlin:

Acumula nombres en una lista

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

Convierta elementos en cadenas y concatenelos, separados por comas

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

Calcular la suma de los salarios del empleado

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

Agrupar empleados por departamento

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

Calcular la suma de salarios por departamento

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

Particionar a los estudiantes para aprobar y reprobar

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

Nombres de miembros masculinos

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

Agrupe los nombres de los miembros en la lista por género

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

Filtrar una lista a otra lista

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

Encontrar la cadena más corta de una lista

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

Contar elementos en una lista después de aplicar el 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') }

y así sucesivamente ... En todos los casos, no se requiere doblar, reducir u otra funcionalidad especial para imitar Stream.collect. Si tiene más casos de uso, agréguelos en los comentarios y ¡podemos ver!

Sobre la pereza

Si desea procesar de forma diferida una cadena, puede convertirla en un Sequenceuso asSequence()antes de la cadena. Al final de la cadena de funciones, generalmente terminas con un Sequencetambién. A continuación, puede utilizar toList(), toSet(), toMap()o alguna otra función para materializar laSequence al final.

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

¿Por qué no hay tipos?

Notará que los ejemplos de Kotlin no especifican los tipos. Esto se debe a que Kotlin tiene inferencia de tipo completa y es completamente segura en tiempo de compilación. Más que Java porque también tiene tipos anulables y puede ayudar a prevenir el temido NPE. Entonces esto en Kotlin:

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

es lo mismo que:

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

Debido a que Kotlin sabe lo que peoplees, y esa people.agees, Intpor lo tanto, la expresión de filtro solo permite la comparación con an Int, y ese people.namees un paso, Stringpor lo tanto, el mappaso produce un List<String>(solo lectura Listde String).

Ahora, si peoplefuera posible null, como en un List<People>?entonces:

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

Devuelve un List<String>?que necesitaría ser anulado ( o usar uno de los otros operadores de Kotlin para valores anulables, vea esta forma idiomática de Kotlin para tratar con valores anulables y también una forma idiomática de manejar la lista nula o vacía en Kotlin )

Ver también:

Jayson Minard
fuente
¿Hay un equivalente al paralelismoStream () de Java8 en Kotlin?
arnab
La respuesta sobre colecciones inmutables y Kotlin es la misma respuesta para @arnab aquí para paralelo, existen otras bibliotecas, úselas
Jayson Minard
2
@arnab Es posible que desee consultar el soporte de Kotlin para las características de Java 7/8 (en particular, kotlinx-support-jdk8) que estuvo disponible a principios de este año: debate.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
roborativo
¿Es realmente idiomático usar 3 referencias diferentes "it" en una sola declaración?
herman
2
Es una preferencia, en las muestras anteriores las mantuve cortas y solo proporcioné un nombre local para un parámetro si es necesario.
Jayson Minard el
47

Para ejemplos adicionales, aquí están todos los ejemplos de Java 8 Stream Tutorial convertidos a Kotlin. El título de cada ejemplo se deriva del artículo fuente:

Cómo funcionan las transmisiones

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

Diferentes tipos de corrientes # 1

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

o cree una función de extensión en String llamada 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)

Ver también: apply()función

Ver también: Funciones de extensión

Ver también: ?.Operador Safe Call , y en general anulabilidad: en Kotlin, ¿cuál es la forma idiomática de tratar con valores anulables, haciendo referencia o convirtiéndolos?

Diferentes tipos de corrientes # 2

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

Diferentes tipos de corrientes # 3

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

Diferentes tipos de corrientes # 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)

Diferentes tipos de corrientes # 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)

Diferentes tipos de corrientes # 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)

Diferentes tipos de corrientes # 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)

Por qué es importante el pedido

Esta sección de Java 8 Stream Tutorial es la misma para Kotlin y Java.

Reutilizando Streams

En Kotlin, depende del tipo de colección si se puede consumir más de una vez. A Sequencegenera un nuevo iterador cada vez y, a menos que afirme "usar solo una vez", puede restablecer el inicio cada vez que se actúa sobre él. Por lo tanto, aunque lo siguiente falla en la secuencia Java 8, pero funciona en 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

Y en Java para obtener el mismo comportamiento:

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

Por lo tanto, en Kotlin, el proveedor de los datos decide si se puede restablecer y proporcionar un nuevo iterador o no. Pero si desea restringir intencionalmente una Sequenceiteración de una vez, puede usar la constrainOnce()función de la Sequencesiguiente manera:

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. 

Operaciones avanzadas

Recopile el ejemplo # 5 (sí, omití los que ya están en la otra respuesta)

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

Y como nota al margen, en Kotlin podemos crear clases de datos simples e instanciar los datos de prueba de la siguiente manera:

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

Recoge el ejemplo # 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 más interesante aquí para Kotlin. Primero, las respuestas incorrectas para explorar las variaciones de la creación Mapde una colección / secuencia:

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

Y ahora para la respuesta correcta:

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

Solo necesitábamos unir los valores coincidentes para colapsar las listas y proporcionar un transformador jointToStringpara pasar de una Personinstancia a otra Person.name.

Recoge el ejemplo # 7

Ok, esto se puede hacer fácilmente sin una costumbre Collector, así que vamos a resolverlo a la manera de Kotlin, luego inventemos un nuevo ejemplo que muestre cómo hacer un proceso similar para el Collector.summarizingIntque no existe de forma nativa en 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(" | ")

¡No es mi culpa que hayan elegido un ejemplo trivial! Ok, aquí hay un nuevosummarizingInt método para Kotlin y una muestra coincidente:

Resumen de ejemplo

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

Pero es mejor crear una función de extensión, 2 para que coincida con los estilos en 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)) }

Ahora tiene dos formas de usar las nuevas summarizingIntfunciones:

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

// or

val stats3 = persons.summarizingInt { it.age }

Y todo esto produce los mismos resultados. También podemos crear esta extensión para trabajarSequence y para los tipos primitivos apropiados.

Por diversión, compare el código JDK de Java con el código personalizado de Kotlin requerido para implementar este resumen.

Jayson Minard
fuente
En la secuencia 5 no hay ninguna ventaja para usar dos mapas en lugar de uno .map { it.substring(1).toInt() }: como bien sabe, el tipo inferido es uno de poder kotlin.
Michele d'Amico
es cierto, pero tampoco hay inconveniente (por comparabilidad, los mantuve separados)
Jayson Minard
Pero el código Java se puede hacer fácilmente paralelo, por lo que en muchos casos sería mejor llamar al código de flujo Java desde Kotlin.
Howard Lovatt
@HowardLovatt hay muchos casos en los que el paralelo no es el camino a seguir, especialmente en entornos concurrentes pesados ​​donde ya estás en un grupo de subprocesos. Apuesto a que el caso de uso promedio NO es paralelo, y es el caso raro. Pero, por supuesto, siempre tiene la opción de usar las clases de Java como mejor le parezca, y nada de esto fue realmente el propósito de esta pregunta y respuesta.
Jayson Minard el
3

Hay algunos casos en los que es difícil evitar llamar collect(Collectors.toList())o algo similar. En esos casos, puede cambiar más rápidamente a un equivalente de Kotlin utilizando funciones de extensión como:

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

Entonces puedes simplemente stream.toList()o stream.asSequence()volver a la API de Kotlin. Un caso como el que Files.list(path)te obliga a entrar Streamcuando no lo deseas, y estas extensiones pueden ayudarte a volver a las colecciones estándar y la API de Kotlin.

Jayson Minard
fuente
2

Más sobre la pereza

Tomemos la solución de ejemplo para "Calcular suma de salarios por departamento" dada por Jayson:

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

Para hacer esto perezoso (es decir, evitar crear un mapa intermedio en el groupBypaso), no es posible usarlo asSequence(). En cambio, debemos usar groupingByy foldoperar:

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

Para algunas personas, esto puede incluso ser más legible, ya que no se trata de entradas de mapa: el it.value parte de la solución también me resultó confusa al principio.

Dado que este es un caso común y preferiríamos no escribir foldcada vez, puede ser mejor simplemente proporcionar una sumByfunción genérica en Grouping:

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

para que podamos simplemente escribir:

val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }
Germán
fuente