Setara dengan Java 8 Stream.collect apa yang tersedia di perpustakaan Kotlin standar?


181

Di Java 8, ada Stream.collectyang memungkinkan agregasi pada koleksi. Di Kotlin, ini tidak ada dengan cara yang sama, selain mungkin sebagai kumpulan fungsi ekstensi di stdlib. Tetapi tidak jelas apa persamaan untuk kasus penggunaan yang berbeda.

Sebagai contoh, di bagian atas JavaDocCollectors adalah contoh yang ditulis untuk Java 8, dan ketika porting mereka ke Kolin Anda tidak dapat menggunakan kelas Java 8 ketika pada versi JDK yang berbeda, sehingga kemungkinan mereka harus ditulis secara berbeda.

Dalam hal sumber daya yang secara online menunjukkan contoh koleksi Kotlin, mereka biasanya sepele dan tidak benar-benar dibandingkan dengan kasus penggunaan yang sama. Apa contoh bagus yang benar-benar cocok dengan kasus-kasus seperti yang didokumentasikan untuk Java 8 Stream.collect? Daftarnya ada:

  • Akumulasi nama menjadi Daftar
  • Akumulasi nama menjadi TreeSet
  • Konversi elemen menjadi string dan menggabungkannya, dipisahkan dengan koma
  • Hitung jumlah gaji karyawan
  • Kelompokkan karyawan menurut departemen
  • Hitung jumlah gaji berdasarkan departemen
  • Partisi siswa menjadi lulus dan gagal

Dengan detail di JavaDoc yang ditautkan di atas.

Catatan: pertanyaan ini sengaja ditulis dan dijawab oleh penulis ( Pertanyaan yang Dijawab Sendiri ), sehingga jawaban idiomatis untuk topik Kotlin yang umum diajukan hadir dalam SO. Juga untuk memperjelas beberapa jawaban lama yang ditulis untuk huruf-huruf Kotlin yang tidak akurat untuk Kotlin saat ini.


Dalam kasus di mana Anda tidak punya pilihan selain menggunakan collect(Collectors.toList())atau serupa, Anda mungkin mendapatkan masalah ini: stackoverflow.com/a/35722167/3679676 (masalah, dengan penyelesaian)
Jayson Minard

Jawaban:


257

Ada fungsi di stdlib Kotlin untuk rata-rata, menghitung, membedakan, memfilter, menemukan, mengelompokkan, bergabung, memetakan, min, maks, mempartisi, mengiris, menyortir, menjumlahkan, menjumlahkan, ke / dari array, ke / dari daftar, ke / dari peta , persatuan, ko-iterasi, semua paradigma fungsional, dan banyak lagi. Jadi Anda dapat menggunakannya untuk membuat 1-liner kecil dan tidak perlu menggunakan sintaks Java 8 yang lebih rumit.

Saya pikir satu-satunya hal yang hilang dari Collectorskelas Java 8 built-in adalah peringkasan (tetapi dalam jawaban lain untuk pertanyaan ini adalah solusi sederhana) .

Satu hal yang hilang dari keduanya adalah batching by count, yang terlihat pada jawaban Stack Overflow lainnya dan memiliki jawaban yang sederhana juga. Kasus lain yang menarik adalah ini juga dari Stack Overflow: Cara idiomatis untuk menumpahkan urutan menjadi tiga daftar menggunakan Kotlin . Dan jika Anda ingin membuat sesuatu seperti Stream.collectuntuk tujuan lain, lihat Custom Stream.collect di Kotlin

EDIT 11.08.2017: Operasi pengumpulan chunked / windowed ditambahkan di kotlin 1.2 M2, lihat https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


Itu selalu baik untuk mengeksplorasi Referensi API untuk kotlin.collections secara keseluruhan sebelum membuat fungsi baru yang mungkin sudah ada di sana.

Berikut adalah beberapa konversi dari Stream.collectcontoh Java 8 ke yang setara di Kotlin:

Akumulasi nama menjadi Daftar

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

Konversi elemen menjadi string dan menggabungkannya, dipisahkan dengan koma

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

Hitung jumlah gaji karyawan

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

Kelompokkan karyawan menurut departemen

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

Hitung jumlah gaji berdasarkan departemen

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

Partisi siswa menjadi lulus dan gagal

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

Nama anggota laki-laki

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

Kelompokkan nama anggota dalam daftar berdasarkan gender

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

Saring daftar ke daftar lain

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

Menemukan string terpendek daftar

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

Menghitung item dalam daftar setelah filter diterapkan

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

dan seterusnya ... Dalam semua kasus, tidak ada lipatan khusus, pengurangan, atau fungsi lain yang diperlukan untuk meniru Stream.collect. Jika Anda memiliki kasus penggunaan lebih lanjut, tambahkan dalam komentar dan kami dapat melihat!

Tentang kemalasan

Jika Anda ingin malas memproses rantai, Anda dapat mengonversi ke Sequencemenggunakan asSequence()sebelum rantai. Pada akhir rantai fungsi, Anda biasanya berakhir dengan Sequencejuga. Maka Anda dapat menggunakan toList(), toSet(), toMap()atau beberapa fungsi lain untuk mewujudkan Sequencedi akhir.

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

Mengapa tidak ada Tipe?!?

Anda akan melihat contoh Kotlin tidak menentukan jenisnya. Ini karena Kotlin memiliki inferensi tipe penuh dan benar-benar tipe aman pada waktu kompilasi. Lebih dari Java karena juga memiliki jenis nullable dan dapat membantu mencegah NPE yang ditakuti. Jadi ini di Kotlin:

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

sama dengan:

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

Karena Kotlin tahu apa peopleadalah, dan bahwa people.ageadalah Intkarena itu ekspresi filter hanya memungkinkan perbandingan ke Int, dan yang people.namemerupakan Stringkarenanya maplangkah menghasilkan List<String>(dibaca Listdari String).

Sekarang, jika peoplemungkin null, as-in a List<People>?then:

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

Mengembalikan nilai List<String>?yang perlu dicentang nol ( atau menggunakan salah satu operator Kotlin lain untuk nilai yang dapat dibatalkan , lihat cara idiomatik Kotlin ini untuk menangani nilai yang dapat dibatalkan dan juga cara Idiomatik menangani daftar kosong atau kosong di Kotlin )

Lihat juga:


Apakah ada yang setara dengan Java8's parallelStream () di Kotlin?
arnab

Jawaban tentang koleksi tidak berubah dan Kotlin adalah jawaban yang sama untuk @arnab di sini untuk paralel, ada perpustakaan lain, gunakan: stackoverflow.com/a/34476880/3679676
Jayson Minard

2
@arnab Anda mungkin ingin melihat fitur Kotlin untuk fitur Java 7/8 (khususnya, kotlinx-support-jdk8) yang tersedia awal tahun ini: mendiskusikan.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
roborative

Apakah benar-benar idiomatis untuk menggunakan 3 referensi "it" yang berbeda dalam satu pernyataan?
herman

2
Ini adalah preferensi, dalam sampel di atas saya membuatnya singkat dan hanya memberikan nama lokal untuk parameter jika perlu.
Jayson Minard

47

Untuk contoh tambahan, berikut adalah semua contoh dari Java 8 Stream Tutorial yang dikonversi ke Kotlin. Judul setiap contoh, berasal dari artikel sumber:

Bagaimana stream bekerja

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

Berbagai Jenis Streaming # 1

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

atau, buat fungsi ekstensi pada String yang disebut 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)

Lihat juga: apply()fungsi

Lihat juga: Fungsi Ekstensi

Lihat juga: ?.Operator Panggilan Aman , dan secara umum tidak dapat dibatalkan: Di Kotlin, apa cara idiomatis untuk menangani nilai yang dapat dibatalkan, merujuk atau mengubahnya

Berbagai Jenis Streaming # 2

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

Berbagai Jenis Streaming # 3

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

Berbagai Jenis Streaming # 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)

Berbagai Jenis Streaming # 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)

Berbagai Jenis Streaming # 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)

Berbagai Jenis Streaming # 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)

Mengapa Memesan Masalah?

Bagian Tutorial Streaming Java 8 ini sama untuk Kotlin dan Java.

Menggunakan Kembali Streaming

Di Kotlin, itu tergantung pada jenis koleksinya apakah dapat dikonsumsi lebih dari satu kali. A Sequencemenghasilkan iterator baru setiap kali, dan kecuali jika menyatakan "gunakan hanya sekali", it dapat mereset ke awal setiap kali itu ditindaklanjuti. Karenanya sementara yang berikut ini gagal di aliran Java 8, tetapi berfungsi di 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

Dan di Jawa untuk mendapatkan perilaku yang sama:

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

Oleh karena itu di Kotlin penyedia data memutuskan apakah ia dapat mengatur ulang dan menyediakan iterator baru atau tidak. Tetapi jika Anda ingin secara sengaja membatasi Sequenceiterasi satu kali, Anda dapat menggunakan constrainOnce()fungsi Sequencesebagai berikut:

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. 

Operasi Lanjutan

Kumpulkan contoh # 5 (ya, saya melewatkan yang sudah ada di jawaban lain)

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

Dan sebagai catatan tambahan, di Kotlin kita dapat membuat kelas data sederhana dan membuat contoh data tes sebagai berikut:

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

Kumpulkan contoh # 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}    

Oke, kasus yang lebih menarik di sini untuk Kotlin. Pertama, jawaban yang salah untuk mengeksplorasi variasi pembuatan Mapdari koleksi / urutan:

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

Dan sekarang untuk jawaban yang benar:

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

Kami hanya perlu bergabung dengan nilai yang cocok untuk menciutkan daftar dan menyediakan transformator jointToStringuntuk berpindah dari Personinstance ke Person.name.

Kumpulkan contoh # 7

Oke, yang ini bisa dengan mudah dilakukan tanpa kebiasaan Collector, jadi mari kita selesaikan dengan cara Kotlin, lalu buat contoh baru yang menunjukkan cara melakukan proses serupa Collector.summarizingIntyang tidak ada di 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(" | ")

Bukan salah saya, mereka mengambil contoh sepele !!! Oke, ini summarizingIntmetode baru untuk Kotlin dan sampel yang cocok:

Contoh Meringkas

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

Tetapi lebih baik untuk membuat fungsi ekstensi, 2 sebenarnya untuk mencocokkan gaya di 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)) }

Sekarang Anda memiliki dua cara untuk menggunakan summarizingIntfungsi - fungsi baru :

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

// or

val stats3 = persons.summarizingInt { it.age }

Dan semua ini menghasilkan hasil yang sama. Kami juga dapat membuat ekstensi ini untuk bekerja pada Sequencedan untuk tipe primitif yang sesuai.

Untuk bersenang-senang, bandingkan kode Java JDK vs kode kustom Kotlin yang diperlukan untuk mengimplementasikan ringkasan ini.


Dalam aliran 5 tidak ada nilai tambah untuk menggunakan dua peta, bukan satu .map { it.substring(1).toInt() }: seperti yang Anda tahu tipe yang disimpulkan adalah salah satu kekuatan kotlin.
Michele d'Amico

benar, tetapi tidak ada sisi buruknya (untuk perbandingan saya memisahkan mereka)
Jayson Minard

Tetapi kode Java dapat dengan mudah dibuat paralel, jadi dalam banyak kasus Anda lebih baik memanggil kode aliran Java dari Kotlin.
Howard Lovatt

@HowardLovatt ada banyak kasus di mana paralel bukan cara untuk pergi, terutama di lingkungan berbarengan yang berat di mana Anda sudah berada di thread pool. Saya yakin case use rata-rata BUKAN paralel, dan ini adalah case yang jarang. Tapi tentu saja, Anda selalu memiliki pilihan untuk menggunakan kelas Java yang Anda inginkan, dan tidak ada yang benar-benar tujuan dari pertanyaan dan jawaban ini.
Jayson Minard

3

Ada beberapa kasus di mana sulit untuk menghindari panggilan collect(Collectors.toList())atau yang serupa. Dalam kasus tersebut, Anda dapat lebih cepat mengubah ke setara Kotlin menggunakan fungsi ekstensi seperti:

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

Kemudian Anda dapat dengan mudah stream.toList()atau stream.asSequence()untuk kembali ke API Kotlin. Sebuah kasus seperti Files.list(path)memaksa Anda menjadi Streamsaat Anda mungkin tidak menginginkannya, dan ekstensi ini dapat membantu Anda untuk beralih kembali ke koleksi standar dan API Kotlin.


2

Lebih lanjut tentang kemalasan

Mari kita ambil contoh solusi untuk "Hitung jumlah gaji berdasarkan departemen" yang diberikan oleh Jayson:

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

Untuk membuat ini malas (yaitu menghindari membuat peta perantara di groupBylangkah), itu tidak mungkin untuk digunakan asSequence(). Sebaliknya, kita harus menggunakan groupingBydan foldmengoperasikan:

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

Bagi sebagian orang ini bahkan mungkin lebih mudah dibaca, karena Anda tidak berurusan dengan entri peta: it.valuebagian dalam solusi itu membingungkan bagi saya juga pada awalnya.

Karena ini adalah kasus umum dan kami memilih untuk tidak menulis foldsetiap kali, mungkin lebih baik memberikan sumByfungsi generik pada Grouping:

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

sehingga kita bisa menulis:

val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.