Quais equivalentes Java 8 Stream.collect estão disponíveis na biblioteca Kotlin padrão?

181

No Java 8, existe o Stream.collectque permite agregações em coleções. No Kotlin, isso não existe da mesma maneira, exceto talvez como uma coleção de funções de extensão no stdlib. Mas não está claro quais são as equivalências para diferentes casos de uso.

Por exemplo, na parte superior do JavaDoc,Collectors há exemplos escritos para Java 8 e, ao portá-los no Kolin, você não pode usar as classes Java 8 quando estiver em uma versão JDK diferente, portanto, provavelmente elas devem ser escritas de maneira diferente.

Em termos de recursos online que mostram exemplos de coleções do Kotlin, eles geralmente são triviais e não se comparam realmente aos mesmos casos de uso. Quais são os bons exemplos que realmente correspondem aos casos documentados para Java 8 Stream.collect? A lista existe:

  • Acumular nomes em uma lista
  • Acumular nomes em um TreeSet
  • Converter elementos em seqüências de caracteres e concatená-los, separados por vírgulas
  • Calcular soma dos salários do empregado
  • Agrupar funcionários por departamento
  • Calcular a soma dos salários por departamento
  • Divida os alunos em aprovados e reprovados

Com detalhes no JavaDoc vinculado acima.

Nota: esta pergunta é intencionalmente escrita e respondida pelo autor ( Perguntas respondidas automaticamente ), para que as respostas idiomáticas aos tópicos mais comuns do Kotlin estejam presentes no SO. Também para esclarecer algumas respostas realmente antigas escritas para alfas do Kotlin que não são precisas para o Kotlin atual.

Jayson Minard
fonte
Nos casos em que você não tem escolha a não ser usar collect(Collectors.toList())ou similar, você pode bater esse problema: stackoverflow.com/a/35722167/3679676 (o problema, com soluções alternativas)
Jayson Minard

Respostas:

257

Existem funções no Kotlin stdlib para média, contagem, distinto, filtragem, localização, agrupamento, junção, mapeamento, min, max, particionamento, fatia, classificação, soma, de / para matrizes, de / para listas, de / para mapas , união, co-iteração, todos os paradigmas funcionais e muito mais. Portanto, você pode usá-los para criar pequenos liners 1 e não há necessidade de usar a sintaxe mais complicada do Java 8.

Acho que a única coisa que falta na Collectorsclasse Java 8 interna é a sumarização (mas em outra resposta a essa pergunta é uma solução simples) .

Uma coisa que falta nos dois é o lote por contagem, que é visto em outra resposta do Stack Overflow e tem uma resposta simples também. Outro caso interessante é este também da Stack Overflow: maneira linguística de espalhar a sequência em três listas usando o Kotlin . E se você deseja criar algo parecido Stream.collectcom outro objetivo, consulte Custom Stream.collect no Kotlin

EDIT 11.08.2017: As operações de coleta em partes / janelas foram adicionadas no kotlin 1.2 M2, consulte https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


É sempre bom explorar a Referência da API para kotlin.collections como um todo antes de criar novas funções que já possam existir lá.

Aqui estão algumas conversões de Stream.collectexemplos do Java 8 para o equivalente no Kotlin:

Acumular nomes em uma lista

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

Converter elementos em seqüências de caracteres e concatená-los, separados por vírgulas

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

Calcular soma dos salários do empregado

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

Agrupar funcionários por departamento

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

Calcular a soma dos salários 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 }}

Divida os alunos em aprovados e reprovados

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

Nomes dos membros 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 nomes de membros da 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 uma lista para outra lista

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

Localizando uma string mais curta

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

Contando itens em uma lista após a aplicação do 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 ... Em todos os casos, nenhuma dobra especial, redução ou outra funcionalidade foi necessária para imitar Stream.collect . Se você tiver outros casos de uso, adicione-os nos comentários e podemos ver!

Sobre preguiça

Se você deseja processar preguiçosamente uma cadeia, pode converter para uma Sequenceutilização asSequence()antes da cadeia. No final da cadeia de funções, você geralmente acaba com um Sequencetambém. Então você pode usar toList(), toSet(), toMap()ou alguma outra função para materializar o Sequenceno 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 que não existem tipos?!?

Você notará que os exemplos do Kotlin não especificam os tipos. Isso ocorre porque o Kotlin tem inferência de tipo completa e é completamente seguro em tempo de compilação. Mais do que Java, porque também possui tipos anuláveis ​​e pode ajudar a evitar o temível NPE. Então, isso em Kotlin:

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

é o mesmo que:

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

Porque Kotlin sabe o que peopleé, e que people.ageé Int, portanto, a expressão de filtro só permite comparação com uma Int, e que people.nameé um String, por conseguinte, o mappasso produz um List<String>(somente leitura ListdeString ).

Agora, se peoplepossível null, como em um List<People>?então:

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

Retorna um List<String>?que precisaria ser verificado como nulo ( ou use um dos outros operadores do Kotlin para obter valores anuláveis; consulte esta maneira idiomática do Kotlin para lidar com valores anuláveis e também a maneira linguística de lidar com lista anulável ou vazia no Kotlin )

Veja também:

Jayson Minard
fonte
Existe um equivalente ao parallelStream () do Java8 no Kotlin?
Arnab
A resposta sobre coleções imutável e Kotlin é a mesma resposta para @arnab aqui para paralelo, existem outras bibliotecas, usá-los: stackoverflow.com/a/34476880/3679676
Jayson Minard
2
@arnab Você pode dar uma olhada no suporte do Kotlin aos recursos do Java 7/8 (em particular, kotlinx-support-jdk8), que foi disponibilizado no início deste ano: discuss.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
roborative
É realmente idiomático usar 3 referências "it" diferentes em uma declaração?
herman
2
É uma preferência, nas amostras acima eu as mantinha curtas e fornecia apenas um nome local para um parâmetro, se necessário.
Jayson Minard
47

Para exemplos adicionais, aqui estão todas as amostras do Java 8 Stream Tutorial convertidas para Kotlin. O título de cada exemplo é derivado do artigo de origem:

Como os fluxos funcionam

// 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 fluxos # 1

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

ou crie uma função de extensão na String chamada 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)

Veja também: apply()function

Veja também: Funções de extensão

Consulte também: ?.Operador de chamada segura e, em geral, anulabilidade: no Kotlin, qual é a maneira idiomática de lidar com valores anuláveis, referenciando-os ou convertendo-os

Diferentes tipos de fluxos # 2

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

Diferentes tipos de fluxos # 3

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

Diferentes tipos de fluxos # 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 fluxos # 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 fluxos # 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 fluxos # 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 que a ordem é importante

Esta seção do Tutorial do Java 8 Stream é a mesma para o Kotlin e o Java.

Reutilizando fluxos

No Kotlin, depende do tipo de coleção se ela pode ser consumida mais de uma vez. A Sequencegera um novo iterador toda vez e, a menos que afirme "usar apenas uma vez", ele pode redefinir o início sempre que é acionado. Portanto, enquanto o seguinte falha no fluxo do Java 8, mas funciona no 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 em Java, para obter o mesmo 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

Portanto, no Kotlin, o provedor dos dados decide se ele pode reiniciar e fornecer um novo iterador ou não. Mas se você deseja restringir intencionalmente uma Sequenceiteração de uma vez, é possível usar a constrainOnce()função da Sequenceseguinte maneira:

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. 

Operações avançadas

Colete o exemplo 5 (sim, eu pulei aqueles que já estão na outra resposta)

// 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 como uma observação lateral, no Kotlin, podemos criar classes de dados simples e instanciar os dados de teste da seguinte maneira:

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

Colete o exemplo # 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, um caso mais interessante aqui para Kotlin. Primeiro, as respostas erradas para explorar as variações da criação de a Mappartir de uma coleção / sequência:

// 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 agora a resposta correta:

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

Nós apenas precisávamos juntar os valores correspondentes para recolher as listas e fornecer um transformador jointToStringpara passar da Personinstância para o Person.name.

Colete o exemplo # 7

Ok, este pode ser feito facilmente sem um costume Collector, então vamos resolvê-lo da maneira Kotlin e então inventar um novo exemplo que mostra como executar um processo semelhante para o Collector.summarizingIntqual não existe originalmente no 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(" | ")

Não é minha culpa que eles tenham escolhido um exemplo trivial !!! Ok, aqui está um novo summarizingIntmétodo para o Kotlin e uma amostra correspondente:

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

Mas é melhor criar uma função de extensão, 2 para corresponder aos estilos no 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)) }

Agora você tem duas maneiras de usar as novas summarizingIntfunções:

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

// or

val stats3 = persons.summarizingInt { it.age }

E tudo isso produz os mesmos resultados. Também podemos criar esta extensão para trabalhar com Sequencetipos primitivos apropriados.

Por diversão, compare o código Java JDK com o código personalizado Kotlin necessário para implementar esta sumarização.

Jayson Minard
fonte
No fluxo 5, não há mais nada para usar dois mapas em vez de um .map { it.substring(1).toInt() }: como você sabe, o tipo bem inferido é um dos poderes do kotlin.
Michele d'Amico
verdade, mas não há nenhuma desvantagem ou (para a comparabilidade eu mantive-los separados)
Jayson Minard
Mas o código Java pode ser facilmente paralelo, portanto, em muitos casos, seria melhor chamar o código de fluxo Java do Kotlin.
21918 Howard Lovatt
@HowardLovatt, existem muitos casos em que o paralelo não é o caminho a seguir, especialmente em ambientes simultâneos pesados ​​em que você já está em um pool de threads. Aposto que o caso de uso médio NÃO é paralelo, e é o caso raro. Mas é claro, você sempre tem a opção de usar classes Java como achar melhor, e nada disso foi realmente o objetivo desta pergunta e resposta.
Jayson Minard
3

Existem alguns casos em que é difícil evitar ligações collect(Collectors.toList())ou algo semelhante. Nesses casos, você pode mudar mais rapidamente para um equivalente Kotlin usando funções de extensão, 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()

Em seguida, você pode simplesmente stream.toList()ou stream.asSequence()voltar para a API Kotlin. Um caso como o Files.list(path)força a entrar Streamquando você não deseja, e essas extensões podem ajudá-lo a voltar às coleções padrão e à API do Kotlin.

Jayson Minard
fonte
2

Mais sobre preguiça

Vamos dar o exemplo de solução para "Calcular a soma dos salários por departamento" fornecida por Jayson:

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

Para tornar isso lento (ou seja, evitar criar um mapa intermediário na groupByetapa), não é possível usá-lo asSequence(). Em vez disso, devemos usar groupingBye foldoperar:

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

Para algumas pessoas, isso pode ser ainda mais legível, já que você não está lidando com entradas do mapa: a it.valueparte da solução também me confundiu no começo.

Como esse é um caso comum e preferimos não escrever a foldcada vez, talvez seja melhor fornecer apenas uma sumByfunção genérica em 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 possamos simplesmente escrever:

val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }
herman
fonte