Kadang kita ingin menulis function yang menerima tipe generic apa pun, tanpa peduli terhadap type argument. Misalnya, function yang mencetak ukuran koleksi tidak peduli apakah koleksi berisi Int, String, atau Pengguna. Untuk kasus ini, Kotlin menyediakan star projection * yang setara dengan List<*>. Star projection memberi tahu kompilator bahwa kita tidak peduli dengan type argument, tetapi tetap memberlakukan batasan variance agar type safety terjaga. Pada chapter ini kita akan memahami star projection, melihat bagaimana cara membaca dan menulis dengan aman, serta membandingkannya dengan List<*> di Java yang dikenal sebagai List<?>.

Apa Itu Star Projection #

Star projection * adalah placeholder untuk type argument yang tidak diketahui. Saat kita menulis List<*>, kompilator memperlakukan list tersebut sebagai List<out Any?> — kita dapat membaca elemen sebagai Any? tetapi tidak dapat menulis elemen baru.

kotlin Star projection dasar
fun cetakUkuran(daftar: List<*>) {
    println("Ukuran: ${daftar.size}")
}

Pada contoh di atas, List<*> menerima List<Int>, List<String>, atau List dari tipe apa pun. Function hanya memanggil daftar.size — tidak memanggil method yang bergantung pada tipe elemen spesifik.

Aturan Star Projection #

Star projection membatasi akses berdasarkan variance:

OperasiList<*>MutableList<*>
Membaca elemenYa, sebagai Any?Ya, sebagai Any?
Menulis elemen baruTidak (List read-only)Tidak (kecuali null)
Method tipe-spesifikTidakTidak

Aturan ini menjaga type safety: karena kompilator tidak tahu type argument aktual, ia tidak dapat memverifikasi bahwa nilai yang akan kita tulis kompatibel.

Contoh Lengkap dengan List dan Wadah #

Mari kita lihat star projection dalam aksi pada berbagai konteks:

kotlin
// Chapter: Star Projections
// List<*> setara dengan List<out Any?>: hanya-baca, tipe elemen tidak diketahui.

fun cetakUkuran(daftar: List<*>) {
    println("Ukuran: ${daftar.size}")
    for (e in daftar) {
        val tipe = if (e == null) "null" else e::class.simpleName
        println("  elemen: $e (tipe runtime: $tipe)")
    }
}

fun jumlahPanjang(daftar: List<*>): Int {
    var total = 0
    for (e in daftar) {
        if (e is CharSequence) total += e.length
    }
    return total
}

class Wadah<T>(var isi: T) {
    fun ambil(): T = isi
}

fun bacaDariWadah(wadah: Wadah<*>) {
    val nilai = wadah.ambil()
    val tipe = if (nilai == null) "null" else nilai::class.simpleName
    println("Nilai: $nilai (tipe: $tipe)")
}

fun main() {
    val daftarInt: List<Int> = listOf(1, 2, 3)
    val daftarStr: List<String> = listOf("Apel", "Mangga", "Jeruk")
    val daftarCampur: List<Any?> = listOf(10, "Halo", null, 3.14)

    cetakUkuran(daftarInt)
    cetakUkuran(daftarStr)
    cetakUkuran(daftarCampur)

    println("Total panjang string di daftarInt: ${jumlahPanjang(daftarInt)}")
    println("Total panjang string di daftarStr: ${jumlahPanjang(daftarStr)}")
    println("Total panjang string di daftarCampur: ${jumlahPanjang(daftarCampur)}")

    val wadahInt: Wadah<Int> = Wadah(42)
    val wadahStr: Wadah<String> = Wadah("Halo")
    bacaDariWadah(wadahInt)
    bacaDariWadah(wadahStr)

    // Map<*, *>
    val peta: Map<String, Int> = mapOf("satu" to 1, "dua" to 2)
    val petaStar: Map<*, *> = peta
    for ((k, v) in petaStar) {
        val tk = if (k == null) "null" else k::class.simpleName
        val tv = if (v == null) "null" else v::class.simpleName
        println("Kunci: $k ($tk), Nilai: $v ($tv)")
    }
}

Output yang Dihasilkan:

text
Ukuran: 3
  elemen: 1 (tipe runtime: Int)
  elemen: 2 (tipe runtime: Int)
  elemen: 3 (tipe runtime: Int)
Ukuran: 3
  elemen: Apel (tipe runtime: String)
  elemen: Mangga (tipe runtime: String)
  elemen: Jeruk (tipe runtime: String)
Ukuran: 4
  elemen: 10 (tipe runtime: Int)
  elemen: Halo (tipe runtime: String)
  elemen: null (tipe runtime: null)
  elemen: 3.14 (tipe runtime: Double)
Total panjang string di daftarInt: 0
Total panjang string di daftarStr: 15
Total panjang string di daftarCampur: 4
Nilai: 42 (tipe: Int)
Nilai: Halo (tipe: String)
Kunci: satu (String), Nilai: 1 (Int)
Kunci: dua (String), Nilai: 2 (Int)

Mari kita telaah baris-baris kode di atas:

  1. fun cetakUkuran(daftar: List<*>): function menerima List<*> — list dari tipe apa pun. Karena List bersifat covariant, List<*> setara dengan List<out Any?>.
  2. for (e in daftar): setiap elemen e bertipe Any? karena type argument tidak diketahui. Kita dapat memanggil method Any? seperti equals, hashCode, dan toString.
  3. e::class.simpleName: mengakses nama kelas runtime melalui refleksi. Hal ini aman karena setiap nilai non-null memiliki kelas runtime yang dapat diperiksa.
  4. fun jumlahPanjang(daftar: List<*>): Int: function memfilter elemen dengan is CharSequence lalu memanggil e.length. Pemeriksaan tipe is memungkinkan smart cast — setelah is CharSequence, kompilator mengizinkan akses ke length.
  5. cetakUkuran(daftarCampur): pemanggilan valid meskipun daftarCampur berisi elemen null dan Double. Function memeriksa null secara eksplisit sebelum mengakses ::class.
  6. fun bacaDariWadah(wadah: Wadah<*>): star projection pada class invariant Wadah. Function dapat memanggil wadah.ambil() (mengembalikan Any?) tetapi tidak dapat memanggil wadah.taruh(...).
  7. val petaStar: Map<*, *> = peta: star projection pada Map dengan dua type parameter. Kedua type parameter diganti *, sehingga kunci dan nilai diperlakukan sebagai Any?.
  8. for ((k, v) in petaStar): destrukturisasi Map.Entry. Setiap k dan v bertipe Any?. Kita memeriksa null sebelum mengakses ::class.

Perbandingan dengan Java ? #

Java memakai wildcard ? untuk tujuan yang sama. List<?> di Java setara dengan List<*> di Kotlin. Perbedaan utama: Java memerlukan tanda tanya eksplisit, sedangkan Kotlin memakai * yang lebih ringkas. Selain itu, Kotlin memungkinkan kombinasi star projection dengan bound, misalnya Comparable<*>.

kotlin Padanan Java wildcard
// Java:  void proses(List<?> daftar)
// Kotlin: fun proses(daftar: List<*>)

Kapan Memakai Star Projection #

Star projection cocok ketika function benar-benar tidak peduli terhadap type argument. Contoh klasik adalah function utilitas seperti cetak, ukuran, atau iterasi yang hanya memanggil method generik. Sebaliknya, jika function perlu memanggil method spesifik tipe, kita harus memakai type parameter biasa dan memeriksa tipe dengan is.

Referensi Lanjutan #

Ketika type argument tidak terlihat oleh kompilator, satu hal yang tidak bisa kita lakukan adalah is List<String> — pemeriksaan tipe generic spesifik tidak mungkin karena type erasure. Pada chapter Type Erasure kita akan memahami mengapa demikian dan bagaimana reified type parameters mengatasi batasan ini.