Map #
Map adalah koleksi pasangan kunci-nilai (key-value pairs) di mana setiap kunci bersifat unik. Ia menjawab pertanyaan “apa nilai yang terhubung dengan kunci ini?” dalam waktu konstan O(1) — jauh lebih efisien dari mencari di list. Seperti List, Kotlin membedakan Map (read-only) dari MutableMap (bisa dimodifikasi) di level tipe. Di balik layar, Kotlin menggunakan implementasi JVM seperti HashMap, LinkedHashMap, atau TreeMap, masing-masing dengan karakteristik yang berbeda. Artikel ini membahas Map secara menyeluruh: dari pembuatan dan akses dasar, berbagai implementasi yang tersedia, operasi fungsional, hingga pola-pola penggunaan yang idiomatis.
Membuat Map #
// mapOf() — immutable, pasangan ditulis dengan infix 'to'
val ibuKota = mapOf(
"Indonesia" to "Jakarta",
"Jepang" to "Tokyo",
"Prancis" to "Paris",
"Brazil" to "Brasilia"
)
// emptyMap() — map kosong
val kosong = emptyMap<String, Int>()
// mutableMapOf() — bisa dimodifikasi
val skor = mutableMapOf(
"Budi" to 85,
"Sari" to 92,
"Ahmad" to 78
)
// buildMap — DSL builder untuk konstruksi yang lebih fleksibel
val konfigurasi = buildMap {
put("host", "localhost")
put("port", "5432")
put("database", "myapp")
if (System.getenv("DEBUG") == "true") {
put("logLevel", "DEBUG")
}
}
Tipe Pasangan Kunci-Nilai #
to adalah fungsi infix yang membuat Pair<K, V>. Kamu bisa juga membuat map dari list of pair:
val pasangan = listOf(
"satu" to 1,
"dua" to 2,
"tiga" to 3
)
val dariPasangan = pasangan.toMap()
// Atau dari dua list terpisah
val kunci = listOf("a", "b", "c")
val nilai = listOf(1, 2, 3)
val dariZip = kunci.zip(nilai).toMap()
println(dariZip) // {a=1, b=2, c=3}
Implementasi Map: Memilih yang Tepat #
Di JVM, ada tiga implementasi utama Map. Kotlin secara default menggunakan LinkedHashMap (lewat mapOf dan mutableMapOf) yang mempertahankan urutan insert.
| Implementasi | Urutan | Performa | Kapan Digunakan |
|---|---|---|---|
HashMap | Tidak dijamin | O(1) rata-rata | Performa maksimal, urutan tidak penting |
LinkedHashMap | Urutan insert (default) | O(1) rata-rata | Ketika urutan insert perlu dipertahankan |
TreeMap | Urutan natural key | O(log n) | Ketika perlu iterasi dalam urutan terurut |
// HashMap — performa, urutan tidak dijamin
val hashMap = HashMap<String, Int>()
hashMap["b"] = 2
hashMap["a"] = 1
hashMap["c"] = 3
println(hashMap) // urutan tidak terduga
// LinkedHashMap — pertahankan urutan insert (default mapOf)
val linkedMap = LinkedHashMap<String, Int>()
linkedMap["b"] = 2
linkedMap["a"] = 1
linkedMap["c"] = 3
println(linkedMap) // {b=2, a=1, c=3} — urutan insert
// TreeMap — urutan natural key (alphabetical untuk String)
val treeMap = java.util.TreeMap<String, Int>()
treeMap["b"] = 2
treeMap["a"] = 1
treeMap["c"] = 3
println(treeMap) // {a=1, b=2, c=3} — urutan alphabet
// sortedMapOf — shortcut untuk TreeMap
val terurut = sortedMapOf("banana" to 2, "apple" to 1, "cherry" to 3)
println(terurut) // {apple=1, banana=2, cherry=3}
Mengakses Elemen #
val populasi = mapOf(
"Jakarta" to 10_500_000,
"Surabaya" to 2_800_000,
"Bandung" to 2_400_000,
"Medan" to 2_200_000
)
// Akses dengan operator [] — kembalikan null jika key tidak ada
println(populasi["Jakarta"]) // 10500000
println(populasi["Bali"]) // null
// get() — identik dengan []
println(populasi.get("Bandung")) // 2400000
// getOrDefault — kembalikan nilai default jika key tidak ada
println(populasi.getOrDefault("Bali", 0)) // 0
// getOrElse — hitung nilai default secara lazy
println(populasi.getOrElse("Bali") {
println("Menghitung populasi default...")
500_000
})
// getValue — lempar NoSuchElementException jika key tidak ada
println(populasi.getValue("Medan")) // 2200000
// populasi.getValue("Bali") // ✗ NoSuchElementException
// getOrPut (hanya MutableMap) — ambil nilai atau sisipkan default
val cache = mutableMapOf<String, Int>()
val hasilHitung = cache.getOrPut("fibonacci-10") {
println("Menghitung fibonacci...")
55 // dihitung hanya sekali, disimpan ke cache
}
println(cache.getOrPut("fibonacci-10") { 99 }) // 55 — dari cache, 99 tidak dihitung
Informasi Struktur Map #
val data = mapOf("a" to 1, "b" to 2, "c" to 3)
println(data.size) // 3
println(data.isEmpty()) // false
println(data.isNotEmpty()) // true
// Akses keys, values, entries
println(data.keys) // [a, b, c]
println(data.values) // [1, 2, 3]
println(data.entries) // [a=1, b=2, c=3]
// Cek keberadaan
println("a" in data) // true (cek key)
println(data.containsKey("z")) // false
println(data.containsValue(2)) // true
Modifikasi MutableMap #
val inventaris = mutableMapOf(
"Laptop" to 10,
"Mouse" to 50,
"Keyboard" to 25
)
// Tambah atau ganti
inventaris["Monitor"] = 8 // tambah key baru
inventaris["Laptop"] = 12 // ganti nilai yang ada
inventaris.put("Headset", 15) // identik dengan []
// Tambah banyak sekaligus
inventaris.putAll(mapOf("Webcam" to 20, "Speaker" to 7))
// Hapus
inventaris.remove("Mouse") // hapus berdasarkan key
inventaris.remove("Monitor", 8) // hapus hanya jika key-value cocok
inventaris.entries.removeIf { it.value < 10 } // hapus berdasarkan kondisi
// Update nilai yang ada
inventaris["Laptop"] = (inventaris["Laptop"] ?: 0) + 5 // tambah 5 dari nilai lama
// merge — gabung dengan logika kustom
inventaris.merge("Keyboard", 10) { lama, tambahan -> lama + tambahan }
// jika "Keyboard" ada: nilai baru = lama + tambahan
// jika tidak ada: tambahkan dengan nilai 10
// compute — hitung ulang nilai untuk key
inventaris.compute("Laptop") { _, stokLama ->
if (stokLama == null) 1 else stokLama + 1
}
println(inventaris)
Iterasi #
val harga = mapOf("Kopi" to 15_000, "Teh" to 10_000, "Jus" to 20_000)
// Destructuring di for loop — paling umum
for ((menu, hargaItem) in harga) {
println("$menu: Rp${"%,d".format(hargaItem)}")
}
// forEach dengan lambda
harga.forEach { (menu, hargaItem) ->
println("$menu → Rp${"%,d".format(hargaItem)}")
}
// Iterasi hanya key atau value
harga.keys.forEach { println(it) }
harga.values.forEach { println(it) }
// Iterasi entries (akses penuh ke Map.Entry)
harga.entries.forEach { entry ->
println("${entry.key}: ${entry.value}")
}
// forEachIndexed tidak ada langsung di Map,
// tapi bisa via entries.forEachIndexed
harga.entries.forEachIndexed { i, (menu, hargaItem) ->
println("$i. $menu: Rp${"%,d".format(hargaItem)}")
}
Transformasi Fungsional #
Semua operasi di bawah menghasilkan map atau koleksi baru — map asli tidak dimodifikasi.
val nilai = mapOf("Matematika" to 85, "Fisika" to 72, "Kimia" to 90, "Biologi" to 68)
// filter — saring berdasarkan entry (key dan value keduanya tersedia)
val lulus = nilai.filter { (_, v) -> v >= 75 }
println(lulus) // {Matematika=85, Kimia=90}
// filterKeys — saring berdasarkan key saja
val sainsKeras = nilai.filterKeys { it in listOf("Fisika", "Kimia") }
println(sainsKeras) // {Fisika=72, Kimia=90}
// filterValues — saring berdasarkan value saja
val nilaiTinggi = nilai.filterValues { it >= 80 }
println(nilaiTinggi) // {Matematika=85, Kimia=90}
// mapValues — transformasi semua value
val nilaiNormalisasi = nilai.mapValues { (_, v) -> v / 100.0 }
println(nilaiNormalisasi) // {Matematika=0.85, Fisika=0.72, ...}
// mapKeys — transformasi semua key
val singkatan = nilai.mapKeys { (k, _) -> k.take(3).uppercase() }
println(singkatan) // {MAT=85, FIS=72, KIM=90, BIO=68}
// map — transformasi entry menjadi list (menghasilkan List, bukan Map)
val deskripsi = nilai.map { (mapel, n) ->
"$mapel: $n (${if (n >= 75) "Lulus" else "Remedial"})"
}
deskripsi.forEach { println(it) }
// any, all, none, count pada Map
println(nilai.any { (_, v) -> v >= 90 }) // true
println(nilai.all { (_, v) -> v >= 60 }) // true
println(nilai.count { (_, v) -> v >= 75 }) // 2
println(nilai.none { (_, v) -> v > 100 }) // true
// Agregasi nilai
println(nilai.values.average()) // 78.75
println(nilai.values.sum()) // 315
println(nilai.maxByOrNull { it.value }) // Kimia=90
println(nilai.minByOrNull { it.value }) // Biologi=68
Menggabungkan Map #
val mapA = mapOf("a" to 1, "b" to 2, "c" to 3)
val mapB = mapOf("b" to 20, "c" to 30, "d" to 40)
// Operator + — gabung, key dari mapB menimpa mapA jika konflik
val gabungan = mapA + mapB
println(gabungan) // {a=1, b=20, c=30, d=40}
// Operator - — hapus key
val dikurangi = mapA - "b"
println(dikurangi) // {a=1, c=3}
val dikurangiBeberapa = mapA - setOf("a", "c")
println(dikurangiBeberapa) // {b=2}
// Gabung dengan logika kustom (resolve konflik secara manual)
fun <K, V> gabungDenganLogika(
mapA: Map<K, V>,
mapB: Map<K, V>,
resolve: (K, V, V) -> V
): Map<K, V> {
val hasil = mapA.toMutableMap()
mapB.forEach { (key, nilaiB) ->
hasil.merge(key, nilaiB) { nilaiA, b -> resolve(key, nilaiA, b) }
}
return hasil
}
// Contoh: gabung stok, ambil nilai terbesar saat konflik
val stokGudangA = mapOf("Laptop" to 5, "Mouse" to 30, "Keyboard" to 10)
val stokGudangB = mapOf("Laptop" to 8, "Monitor" to 15, "Keyboard" to 7)
val totalStok = gabungDenganLogika(stokGudangA, stokGudangB) { _, a, b -> a + b }
println(totalStok) // {Laptop=13, Mouse=30, Keyboard=17, Monitor=15}
Konversi dari Koleksi Lain #
data class Mahasiswa(val nim: String, val nama: String, val ipk: Double)
val daftarMahasiswa = listOf(
Mahasiswa("2021001", "Budi Santoso", 3.85),
Mahasiswa("2021002", "Sari Dewi", 3.92),
Mahasiswa("2021003", "Ahmad Fauzi", 3.71)
)
// associateBy — buat Map dengan key dari transformasi elemen
val mapByNim = daftarMahasiswa.associateBy { it.nim }
println(mapByNim["2021001"]?.nama) // Budi Santoso
// associateWith — buat Map dengan value dari transformasi elemen
val mapNamaKeIpk = daftarMahasiswa.associateWith { it.ipk }
// Map<Mahasiswa, Double> — objek Mahasiswa sebagai key
// associate — bebas tentukan key dan value
val nimKeNama = daftarMahasiswa.associate { it.nim to it.nama }
println(nimKeNama) // {2021001=Budi Santoso, 2021002=Sari Dewi, ...}
// groupBy — kelompokkan jadi Map<K, List<V>>
val mhsPerAngkatan = daftarMahasiswa.groupBy { it.nim.take(4) }
mhsPerAngkatan.forEach { (angkatan, list) ->
println("Angkatan $angkatan: ${list.map { it.nama }}")
}
// toMap dari list of Pair
val pasangan = listOf("x" to 10, "y" to 20, "z" to 30)
val mapDariPasangan = pasangan.toMap()
println(mapDariPasangan) // {x=10, y=20, z=30}
Pola Penggunaan Umum #
Cache / Memo Table #
// Memoization — simpan hasil komputasi mahal
val cache = mutableMapOf<Int, Long>()
fun fibonacci(n: Int): Long {
if (n <= 1) return n.toLong()
return cache.getOrPut(n) {
fibonacci(n - 1) + fibonacci(n - 2)
}
}
println(fibonacci(50)) // 12586269025 — cepat karena di-cache
Frekuensi / Histogram #
val teks = "hello world kotlin is awesome kotlin is great"
val kata = teks.split(" ")
// Hitung frekuensi setiap kata
val frekuensi = kata.groupingBy { it }.eachCount()
println(frekuensi)
// {hello=1, world=1, kotlin=2, is=2, awesome=1, great=1}
// Atau dengan fold
val frekuensiManual = kata.fold(mutableMapOf<String, Int>()) { acc, w ->
acc.apply { merge(w, 1, Int::plus) }
}
// Urutkan berdasarkan frekuensi tertinggi
val terurut = frekuensi.entries
.sortedByDescending { it.value }
.take(3)
.joinToString { "${it.key}=${it.value}" }
println("Top 3: $terurut") // Top 3: kotlin=2, is=2, hello=1
Konfigurasi dengan Default #
// Map sebagai konfigurasi yang bisa di-override
val konfigurasiDefault = mapOf(
"host" to "localhost",
"port" to "5432",
"poolSize" to "10",
"timeout" to "30000"
)
val konfigurasiKustom = mapOf(
"host" to "db.production.com",
"poolSize" to "50"
)
// Gabung: default ditimpa oleh kustom
val konfigurasiAkhir = konfigurasiDefault + konfigurasiKustom
println(konfigurasiAkhir)
// {host=db.production.com, port=5432, poolSize=50, timeout=30000}
Indeks Terbalik #
val nikKeNama = mapOf(
"3201234567890001" to "Budi Santoso",
"3201234567890002" to "Sari Dewi",
"3201234567890003" to "Ahmad Fauzi"
)
// Balik map: nama menjadi key, NIK menjadi value
val namaKeNik = nikKeNama.entries.associate { (nik, nama) -> nama to nik }
println(namaKeNik["Sari Dewi"]) // 3201234567890002
Ringkasan #
mapOfsebagai default — selalu mulai dengan map immutable. Ganti kemutableMapOfhanya jika memang perlu dimodifikasi. SepertiList, ekspos sebagaiMapke luar kelas, simpan sebagaiMutableMapsecara internal.- Akses aman dengan
getOrDefaultataugetOrElse— hindarigetValuekecuali kamu yakin key pasti ada.map[key]mengembalikan null untuk key yang tidak ada — tangani dengan Elvis?: 0ataugetOrDefault.getOrPutuntuk pola cache — ambil nilai yang ada atau sisipkan dan kembalikan nilai baru. Ini adalah pola memoization yang sangat umum dan bersih.LinkedHashMapadalah default —mapOfdanmutableMapOfmenggunakanLinkedHashMapyang mempertahankan urutan insert. GunakansortedMapOfuntuk urutan natural key, atauHashMapeksplisit jika urutan benar-benar tidak penting.filterKeys,filterValues,mapKeys,mapValues— gunakan versi yang tepat sesuai dimensi yang kamu operasikan. Lebih ekspresif darifiltergenerik yang harus destructuring setiap kali.- Operator
+untuk menggabungkan —mapA + mapBmenghasilkan map baru di mana key yang sama dimapBmenimpamapA. Berguna untuk pola konfigurasi default + override.associateByuntuk indexing — ubah list menjadi map denganassociateBy { it.id }untuk akses O(1) berdasarkan identifier. Jauh lebih efisien darilist.find { it.id == targetId }yang O(n).groupByuntuk agregasi — hasilkanMap<K, List<V>>untuk mengelompokkan data. Kombinasikan denganmapValues { it.value.sumOf {...} }untuk agregasi per kelompok.