Agregasi adalah proses meringkas collection menjadi satu nilai atau objek kecil. Dari daftar transaksi, kita mungkin ingin tahu total pendapatan, jumlah produk, harga tertinggi, atau rata-rata qty per produk. Pada chapter ini kita akan mempelajari operasi agregasi utama: sum, sumOf, average, maxOrNull, minOrNull, maxByOrNull, minByOrNull, reduce, dan fold. Kita juga akan melihat pola agregasi multi-nilai yang menggunakan satu fold untuk menghitung beberapa statistik sekaligus.

Operasi Numerik Dasar #

Untuk collection tipe Number, tersedia operasi numerik langsung:

OperasiMengembalikanCatatan
sum()Int, Long, DoubleTotal elemen
average()DoubleRata-rata; NaN untuk empty
maxOrNull()T?Elemen terbesar atau null
minOrNull()T?Elemen terkecil atau null
maxByOrNull { sel }T?Elemen dengan selector terbesar
minByOrNull { sel }T?Elemen dengan selector terkecil
maxOfOrNull { sel }R?Nilai selector terbesar
minOfOrNull { sel }R?Nilai selector terkecil

maxOf dan minOf tanpa OrNull melempar NoSuchElementException pada collection kosong.

sumOf: Selector Numerik Ringkas #

sumOf { it.qty } menggantikan pola map { it.qty }.sum() dengan satu iterasi. Tersedia overload untuk Int, Long, Double, BigInteger, dan tipe numerik lain.

kotlin Contoh: sumOf
data class Penjualan(val produk: String, val qty: Int, val harga: Int)

val totalQty = data.sumOf { it.qty }
val totalPendapatan = data.sumOf { it.qty * it.harga }

sumOf lebih efisien daripada map.sum() karena tidak mengalokasikan list sementara.

reduce vs fold #

reduce dan fold adalah dua operasi paling fleksibel. Perbedaan utama:

Aspekreducefold
Nilai awalElemen pertamaEksplisit (bisa berbeda tipe)
Tipe hasilSama dengan elemenBebas, mengikuti nilai awal
Pada collection kosongMelempar eksepsiMengembalikan nilai awal
kotlin Contoh: reduce dan fold
val totalReduce = listOf(1, 2, 3, 4).reduce { acc, x -> acc + x }       // 10
val totalFold = listOf(1, 2, 3, 4).fold(0) { acc, x -> acc + x }       // 10
val ringkasan = listOf(1, 2, 3).fold("Deret:") { acc, x -> "$acc $x" } // "Deret: 1 2 3"

Contoh Lengkap #

kotlin
data class Penjualan(val produk: String, val qty: Int, val harga: Int) {
    val subtotal get() = qty * harga
}

fun main() {
    val data = listOf(
        Penjualan("Apel", 3, 5000),
        Penjualan("Jeruk", 5, 4000),
        Penjualan("Pisang", 10, 3000),
        Penjualan("Mangga", 2, 8000)
    )

    // sum: hanya untuk collection of numbers
    val qtyList = data.map { it.qty }
    println("Sum qty: ${qtyList.sum()}")

    // sumOf: ringkas untuk selector numerik
    val totalQty = data.sumOf { it.qty }
    val totalSubtotal = data.sumOf { it.subtotal }
    println("Total qty: $totalQty")
    println("Total subtotal: $totalSubtotal")

    // average
    val avgQty = qtyList.average()
    println("Rata-rata qty: $avgQty")

    val avgSubtotal = data.map { it.subtotal }.average()
    println("Rata-rata subtotal: $avgSubtotal")

    // maxOrNull / minOrNull
    val maxQty = data.maxOfOrNull { it.qty }
    val minQty = data.minOfOrNull { it.qty }
    println("Max qty: $maxQty, Min qty: $minQty")

    val maxProdukByQty = data.maxByOrNull { it.qty }
    println("Produk qty tertinggi: $maxProdukByQty")

    val minProdukBySubtotal = data.minByOrNull { it.subtotal }
    println("Produk subtotal terendah: $minProdukBySubtotal")

    // maxOf / minOf (melempar pada koleksi kosong)
    val dataIsi = listOf(5, 2, 8, 1, 9)
    println("maxOf: ${dataIsi.maxOf { it }}")
    println("minOf: ${dataIsi.minOf { it }}")

    // reduce: akumulasi tanpa nilai awal
    val totalReduce = data.map { it.subtotal }.reduce { acc, x -> acc + x }
    println("Total via reduce: $totalReduce")

    // fold: akumulasi dengan nilai awal dan tipe bebas
    val ringkasan = data.fold("Ringkasan: ") { acc, p -> "$acc${p.produk}=${p.subtotal}; " }
    println(ringkasan)

    // count
    println("Jumlah produk: ${data.count()}")
    println("Jumlah produk qty >=5: ${data.count { it.qty >= 5 }}")

    // Contoh agregasi multi-nilai dengan fold
    data class Stat(val total: Int, val max: Int, val min: Int)
    val stat = data.fold(Stat(0, Int.MIN_VALUE, Int.MAX_VALUE)) { acc, p ->
        Stat(
            total = acc.total + p.subtotal,
            max = maxOf(acc.max, p.subtotal),
            min = minOf(acc.min, p.subtotal)
        )
    }
    println("Statistik: $stat")

    // Empty collection
    val kosong = emptyList<Int>()
    println("Empty sum: ${kosong.sum()}")
    println("Empty maxOrNull: ${kosong.maxOrNull()}")
    println("Empty average: ${kosong.average()}")
    // println("Empty maxOf: ${kosong.maxOf { it }}")  // melempar NoSuchElementException
}

Output yang Dihasilkan:

text
Sum qty: 20
Total qty: 20
Total subtotal: 81000
Rata-rata qty: 5.0
Rata-rata subtotal: 20250.0
Max qty: 10, Min qty: 2
Produk qty tertinggi: Penjualan(produk=Pisang, qty=10, harga=3000)
Produk subtotal terendah: Penjualan(produk=Apel, qty=3, harga=5000)
maxOf: 9
minOf: 1
Total via reduce: 81000
Ringkasan: Apel=15000; Jeruk=20000; Pisang=30000; Mangga=16000; 
Jumlah produk: 4
Jumlah produk qty >=5: 2
Statistik: Stat(total=81000, max=30000, min=15000)
Empty sum: 0
Empty maxOrNull: null
Empty average: NaN

Mari kita bedah baris-baris penting di atas:

  1. data.sumOf { it.qty }: menjumlahkan qty tanpa membuat list antara. Bila kita menulis data.map { it.qty }.sum(), ada alokasi list tambahan; sumOf menghindari itu.
  2. qtyList.average(): mengembalikan Double — selalu Double, bahkan bila elemen Int. Pada list kosong, hasilnya NaN (bukan eksepsi).
  3. data.maxOfOrNull { it.qty }: versi aman yang mengembalikan Int? — null bila list kosong. maxOf tanpa OrNull akan melempar NoSuchElementException.
  4. data.maxByOrNull { it.qty }: mengembalikan elemen (objek Penjualan) yang memiliki qty tertinggi, bukan nilai qty itu sendiri.
  5. data.fold("Ringkasan: ") { acc, p -> ... }: tipe akumulator String, berbeda dari tipe elemen Penjualan. Inilah keunggulan fold dibanding reduce.
  6. data.fold(Stat(0, ...)) { acc, p -> Stat(...) }: pola agregasi multi-nilai. Satu iterasi menghasilkan tiga statistik sekaligus: total, max, dan min. Pola ini lebih efisien daripada tiga fold terpisah yang masing-masing mengiterasi list.
  7. kosong.average() mengembalikan NaN: pilihan desain pustaka standar untuk menghindari eksepsi pada collection kosong. Selalu periksa isEmpty() sebelum memproses rata-rata bila ingin menangani kasus ini secara khusus.

Referensi Lanjutan #

Setelah memahami agregasi, kita akan melihat lebih dekat cara iterasi collection — dari for tradisional hingga iterator protocol — pada chapter berikutnya.