Tipe koleksi Kotlin (List, MutableList, Map) adalah tipe alias untuk java.util.List dan java.util.Map di bytecode JVM. Hal ini membuat interoperabilitas koleksi antar dua bahasa menjadi nyaris transparan — ArrayList<Int> Java dapat dipass langsung ke parameter List<Int> Kotlin. Namun transparansi ini menyembunyikan perbedaan penting: List Kotlin bersifat read-only di level kompilator, sedangkan java.util.List Java tetap mutable. Bab ini membahas perbedaan tersebut, konversi eksplisit, dan jebakan platform type pada elemen koleksi.

Tiga Aturan Interoperabilitas Koleksi #

Tipe KotlinPadanan JavaMutability di Java
List<T> (read-only)java.util.List<T>Tetap mutable!
MutableList<T>java.util.List<T>Mutable (konsisten)
Map<K, V> (read-only)java.util.Map<K, V>Tetap mutable!
MutableMap<K, V>java.util.Map<K, V>Mutable (konsisten)
Set<T> (read-only)java.util.Set<T>Tetap mutable!

Aturan kunci: List Kotlin hanya bersifat read-only dari perspektif Kotlin. Di bytecode Java, method add(), remove(), set() tetap ada di class. Kode Java yang menerima List Kotlin tetap dapat memutasi koleksi — kompilator Kotlin tidak dapat mencegahnya.

Contoh Lengkap: Konversi, View, dan Platform Type #

Kode berikut mendemonstrasikan empat skenario: List read-only dipass ke function, MutableList dimutasi, konversi eksplisit toList()/toMutableList(), dan platform type pada elemen:

kotlin
import java.util.ArrayList
import java.util.HashMap

// Demonstrasi interoperabilitas koleksi Java <-> Kotlin:
// - Kotlin List<T> (read-only) <-> java.util.List<T> (mutable di Java)
// - Kotlin MutableList<T> <-> java.util.List<T> (mutable)
// - Elemen dari koleksi Java adalah platform type T!
// - Konversi eksplisit: toList(), toMutableList(), asList()

// Function Kotlin yang menerima List read-only (secara Java: List yang bisa dimutasi)
fun hitungTotal(harga: List<Int>): Int {
    return harga.sum()
}

// Function Kotlin yang menerima MutableList
fun tambahDiskon(harga: MutableList<Int>) {
    for (i in harga.indices) {
        harga[i] = harga[i] - (harga[i] / 10)  // diskon 10%
    }
}

// Function Kotlin yang menerima java.util.ArrayList eksplisit
fun prosesArrayListJava(daftar: ArrayList<String>): Int {
    return daftar.size
}

fun main() {
    // 1. List Kotlin read-only vs ArrayList Java
    val listKotlin: List<Int> = listOf(100, 200, 300)
    val arrJava: ArrayList<Int> = ArrayList()
    arrJava.add(100)
    arrJava.add(200)
    arrJava.add(300)

    println("=== List Kotlin vs ArrayList Java ===")
    println("listKotlin          : $listKotlin (${listKotlin::class.simpleName})")
    println("arrJava             : $arrJava (${arrJava::class.simpleName})")

    // 2. List Kotlin dapat dipass ke function yang minta ArrayList? Tidak langsung.
    // ArrayList Java dapat dipass ke function yang minta List Kotlin? Ya.
    println()
    println("=== ArrayList Java -> List Kotlin ===")
    println("hitungTotal(arrJava) : ${hitungTotal(arrJava)}")
    // arrJava adalah ArrayList yang IS-A List, jadi accepted.

    // 3. MutableList Kotlin dapat dimodifikasi
    println()
    println("=== MutableList dimodifikasi ===")
    val harga: MutableList<Int> = mutableListOf(1000, 2000, 3000)
    println("sebelum  : $harga")
    tambahDiskon(harga)
    println("setelah  : $harga")

    // 4. List read-only TIDAK dapat dipass ke MutableList parameter
    // hitungTotal(listKotlin)  // OK
    // tambahDiskon(listKotlin) // COMPILE ERROR: List<Int> not MutableList<Int>

    // 5. Konversi eksplisit: List <-> MutableList
    println()
    println("=== Konversi eksplisit ===")
    val readOnly: List<Int> = listOf(1, 2, 3)
    val mutable: MutableList<Int> = readOnly.toMutableList()
    mutable.add(4)
    println("readOnly : $readOnly")
    println("mutable  : $mutable")

    val readOnlyBaru: List<Int> = mutable.toList()
    println("backToRO : $readOnlyBaru")

    // 6. Elemen dari koleksi Java adalah platform type
    println()
    println("=== Platform type dari koleksi Java ===")
    val peta: HashMap<String, String?> = HashMap()
    peta["a"] = "Budi"
    peta["b"] = null

    // elemen dari peta["a"] adalah platform type (bisa null)
    for (k in listOf("a", "b", "c")) {
        val nilai: String? = peta[k]
        println("peta[$k] = ${nilai ?: "null (tidak ada)"}")
    }

    // 7. ArrayList Java mutable -> Kotlin view read-only via toList()
    println()
    println("=== toList(): snapshot read-only ===")
    val arr: ArrayList<Int> = ArrayList()
    arr.add(10); arr.add(20); arr.add(30)
    val viewReadOnly: List<Int> = arr.toList()  // snapshot, tidak bisa dimutasi

    // arr masih bisa dimutasi, viewReadOnly tidak bisa
    arr.add(40)
    println("arr             : $arr")
    println("viewReadOnly    : $viewReadOnly")
    // viewReadOnly.add(50) // ERROR: List tidak punya add()

    // 8. Tipe primitif vs boxed di koleksi Java
    println()
    println("=== Primitif vs boxed ===")
    val ints: MutableList<Int> = mutableListOf(1, 2, 3)
    println("ints class       : ${ints::class}")
}

Output yang Dihasilkan:

text
=== List Kotlin vs ArrayList Java ===
listKotlin          : [100, 200, 300] (ArrayList)
arrJava             : [100, 200, 300] (ArrayList)

=== ArrayList Java -> List Kotlin ===
hitungTotal(arrJava) : 600

=== MutableList dimodifikasi ===
sebelum  : [1000, 2000, 3000]
setelah  : [900, 1800, 2700]

=== Konversi eksplisit ===
readOnly : [1, 2, 3]
mutable  : [1, 2, 3, 4]
backToRO : [1, 2, 3, 4]

=== Platform type dari koleksi Java ===
peta[a] = Budi
peta[b] = null (tidak ada)
peta[c] = null (tidak ada)

=== toList(): snapshot read-only ===
arr             : [10, 20, 30, 40]
viewReadOnly    : [10, 20, 30]

=== Primitif vs boxed ===
ints class       : class java.util.ArrayList

Mari kita telaah baris-baris kode di atas:

  1. Baris 17 (fun hitungTotal(harga: List<Int>)): parameter bertipe List<Int> read-only di Kotlin. Secara bytecode, List<Int> Kotlin adalah java.util.List<Integer> Java. Method add(), remove(), set() tidak dipanggil dari dalam function — kompilator Kotlin melarangnya. Namun kode Java yang memanggil function ini tetap dapat memutasi list sebelumnya.
  2. Baris 23 (fun tambahDiskon(harga: MutableList<Int>)): parameter MutableList<Int> eksplisit mengizinkan mutasi. Sisi Java yang memanggil function ini harus menyadari bahwa list yang diberikan akan dimutasi. Kontrak ini dibuat eksplisit di tipe parameter, bukan di dokumentasi.
  3. Baris 41-42 (listKotlin::class.simpleName menghasilkan “ArrayList”): listOf(100, 200, 300) di bytecode JVM menghasilkan instance java.util.ArrayList — tipe konkret sama dengan ArrayList Java. Perbedaan List dan MutableList hanya di level tipe Kotlin, bukan di runtime.
  4. Baris 51 (hitungTotal(arrJava)): ArrayList<Int> Java adalah List<Int> (IS-A), sehingga dapat dipass ke parameter List<Int> Kotlin. Tidak ada konversi atau copy. Hal yang sama berlaku sebaliknya: MutableList<Int> Kotlin dapat dipass ke method Java yang menerima java.util.List<Integer>.
  5. Baris 68 (readOnly.toMutableList()): konversi eksplisit membuat salinan mutable dari list read-only. Modifikasi pada mutable tidak memengaruhi readOnly karena data disalin. Operasi ini O(n).
  6. Baris 76 (peta["a"] mengembalikan platform type): elemen dari HashMap<String, String?> adalah String? di Kotlin karena tipe generik V adalah String?. Bila tipe generik Java adalah V tanpa nullability annotation, elemennya menjadi platform type String!. Inilah jebakan klasik: kode Kotlin yang mengasumsikan elemen non-null dapat melempar NPE bila koleksi Java ternyata berisi null.
  7. Baris 84 (arr.toList() membuat snapshot): toList() menyalin elemen ke list baru. Modifikasi pada arr setelahnya tidak memengaruhi viewReadOnly. Ini berbeda dari asList() Java yang membuat view hidup (live view). Kotlin sengaja memakai salinan untuk menjaga immutability.
  8. Baris 99 (ints::class menghasilkan class java.util.ArrayList): bukti konkret bahwa mutableListOf(1, 2, 3) di bytecode JVM adalah instance java.util.ArrayList. Tidak ada wrapper Kotlin di runtime — semuanya tipe Java standar.

Strategi Konversi dan Salinan #

OperasiImplementasiKompleksitasDampak pada Asal
list.toList()Salin elemen ke List baruO(n)Tidak ada (asal tak tersentuh)
list.toMutableList()Salin elemen ke MutableList baruO(n)Tidak ada
list.toSet()Salin elemen ke Set baruO(n)Tidak ada
list.toCollection(dest)Tambah elemen ke koleksi tujuanO(n)Tujuan dimutasi
mutableList.toList()Salinan read-onlyO(n)Tidak ada

Referensi Lanjutan #

Selanjutnya #

Bab penutup section ini merangkum praktik terbaik interoperabilitas: anotasi nullability di Java, @JvmStatic untuk companion method, deklarasi tipe eksplisit untuk platform type, dan strategi pengujian dari kedua sisi. Bab Interoperability Best Practices akan menyatukan semuanya.