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.
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:
| Operasi | List<*> | MutableList<*> |
|---|---|---|
| Membaca elemen | Ya, sebagai Any? | Ya, sebagai Any? |
| Menulis elemen baru | Tidak (List read-only) | Tidak (kecuali null) |
| Method tipe-spesifik | Tidak | Tidak |
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:
// 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:
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:
fun cetakUkuran(daftar: List<*>): function menerimaList<*>— list dari tipe apa pun. Karena List bersifat covariant,List<*>setara denganList<out Any?>.for (e in daftar): setiap elemenebertipeAny?karena type argument tidak diketahui. Kita dapat memanggil methodAny?sepertiequals,hashCode, dantoString.e::class.simpleName: mengakses nama kelas runtime melalui refleksi. Hal ini aman karena setiap nilai non-null memiliki kelas runtime yang dapat diperiksa.fun jumlahPanjang(daftar: List<*>): Int: function memfilter elemen denganis CharSequencelalu memanggile.length. Pemeriksaan tipeismemungkinkan smart cast — setelahis CharSequence, kompilator mengizinkan akses kelength.cetakUkuran(daftarCampur): pemanggilan valid meskipundaftarCampurberisi elemennulldanDouble. Function memeriksanullsecara eksplisit sebelum mengakses::class.fun bacaDariWadah(wadah: Wadah<*>): star projection pada class invariantWadah. Function dapat memanggilwadah.ambil()(mengembalikanAny?) tetapi tidak dapat memanggilwadah.taruh(...).val petaStar: Map<*, *> = peta: star projection padaMapdengan dua type parameter. Kedua type parameter diganti*, sehingga kunci dan nilai diperlakukan sebagaiAny?.for ((k, v) in petaStar): destrukturisasiMap.Entry. SetiapkdanvbertipeAny?. Kita memeriksanullsebelum 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<*>.
// 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 #
- Generics: Variance
— bagian star projections (
*) pada panduan variance resmi
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.