Declaration-site variance menyelesaikan banyak kasus, tetapi tidak semua class dapat dideklarasikan covariant atau contravariant secara keseluruhan. Beberapa class secara alami invariant karena memproduksi dan mengkonsumsi T. Namun, pada pemakaian tertentu, kita mungkin hanya membaca atau hanya menulis — di sinilah use-site variance berguna. Use-site variance berarti kita menentukan variance pada saat pemakaian tipe dengan out atau in projection. Pada chapter ini kita akan mempelajari cara memakai use-site variance, melihat kapan diperlukan, serta memahami padanannya di Java yaitu wildcard ? extends dan ? super.
Mengapa Use-Site Variance Diperlukan #
Bayangkan class Wadah<T>(var isi: T) yang dapat membaca dan menulis T. Class ini tidak dapat dideklarasikan covariant (<out T>) karena var menempatkan T di posisi in. Namun, dalam sebuah function yang hanya membaca isi, kita tahu bahwa menulis tidak akan terjadi. Untuk kasus ini, kita menentukan variance pada parameter function:
fun lihatIsi(wadah: Wadah<out Hewan>): String {
return "Isi: ${wadah.ambil().nama}"
}Tanda out Hewan pada signature function memberitahu kompilator bahwa di dalam function ini, wadah hanya akan dipakai untuk membaca. Akibatnya, pemanggil dapat meneruskan Wadah<Kucing> atau Wadah<Anjing> ke function ini.
Out Projection dan In Projection #
Use-site variance memakai dua jenis projection:
| Projection | Arti | Padanan Java |
|---|---|---|
Wadah<out T> | Hanya membaca T dari wadah | Wadah<? extends T> |
Wadah<in T> | Hanya menulis T ke wadah | Wadah<? super T> |
out T membatasi akses: kita dapat memanggil method yang mengembalikan T, tetapi tidak dapat memanggil method yang menerima T sebagai parameter. Sebaliknya, in T membatasi akses ke sebaliknya.
Contoh Lengkap dengan Wadah Invariant #
Mari kita lihat bagaimana use-site variance memungkinkan kita memakai Wadah<T> yang invariant dengan aman pada konteks pembacaan dan penulisan:
// Chapter: Use-Site Variance
// Variance ditentukan saat pemakaian tipe dengan `out` dan `in` projections.
interface Hewan {
val nama: String
}
class Kucing(override val nama: String) : Hewan
class Anjing(override val nama: String) : Hewan
// Wadah bersifat invariant karena T dipakai di posisi in (set) dan out (get).
class Wadah<T>(var isi: T) {
fun ambil(): T = isi
fun taruh(item: T) {
isi = item
}
}
// Use-site covariance: hanya membaca dari wadah, tidak menulis.
fun lihatIsi(wadah: Wadah<out Hewan>): String {
return "Isi: ${wadah.ambil().nama}"
}
// Use-site contravariance: hanya menulis ke wadah, tidak membaca nilai spesifik.
fun isiDenganHewan(wadah: Wadah<in Hewan>): Wadah<Hewan> {
wadah.taruh(Kucing("Default"))
@Suppress("UNCHECKED_CAST")
return wadah as Wadah<Hewan>
}
fun salinDariKe(dari: Wadah<out Hewan>, ke: Wadah<in Hewan>) {
ke.taruh(dari.ambil())
}
fun main() {
val wadahKucing: Wadah<Kucing> = Wadah(Kucing("Miko"))
val wadahAnjing: Wadah<Anjing> = Wadah(Anjing("Rex"))
// out projection: Wadah<Kucing> bisa dianggap Wadah<out Hewan>
println(lihatIsi(wadahKucing))
println(lihatIsi(wadahAnjing))
// in projection: Wadah<Hewan> bisa diisi dengan Hewan apa saja
val wadahHewanKosong: Wadah<Hewan> = Wadah(Kucing("Sementara"))
val terisi = isiDenganHewan(wadahHewanKosong)
println("Setelah isiDenganHewan: ${terisi.ambil().nama}")
// Kombinasi: salin dari Wadah<Kucing> ke Wadah<Hewan>
val sumber: Wadah<Kucing> = Wadah(Kucing("Tom"))
val tujuan: Wadah<Hewan> = Wadah(Anjing("Placeholder"))
salinDariKe(sumber, tujuan)
println("Sumber setelah salin: ${sumber.ambil().nama}")
println("Tujuan setelah salin: ${tujuan.ambil().nama}")
}Output yang Dihasilkan:
Isi: Miko
Isi: Rex
Setelah isiDenganHewan: Default
Sumber setelah salin: Tom
Tujuan setelah salin: TomMari kita telaah baris-baris kode di atas:
class Wadah<T>(var isi: T): class invariant.Tmuncul di posisivar(in & out). Tidak dapat dideklarasikan<out T>atau<in T>.fun lihatIsi(wadah: Wadah<out Hewan>): use-siteoutprojection. Function hanya memanggilwadah.ambil()— tidak memanggiltaruh. Karena variance ditentukan pada signature, pemanggil dapat meneruskanWadah<Kucing>atauWadah<Anjing>.fun isiDenganHewan(wadah: Wadah<in Hewan>): use-siteinprojection. Function hanya memanggilwadah.taruh(Kucing(...)). Pemanggil dapat meneruskanWadah<Hewan>atauWadah<Any>.val terisi = isiDenganHewan(wadahHewanKosong): pemanggilan valid.Wadah<Hewan>cocok dengan parameterWadah<in Hewan>.@Suppress("UNCHECKED_CAST"): castWadah<Hewan>dariWadah<in Hewan>memerlukan penenangan peringatan kompilator. Cast ini aman karena kita baru saja menulisKucingke wadah, sehingga isinya pastiHewan.fun salinDariKe(dari: Wadah<out Hewan>, ke: Wadah<in Hewan>): kombinasioutdaninprojection.darihanya dibaca,kehanya ditulis. Function ini bekerja untukWadah<Kucing>sebagai sumber danWadah<Hewan>sebagai tujuan.salinDariKe(sumber, tujuan): pemanggilan valid.Wadah<Kucing>cocok denganWadah<out Hewan>karenaKucingadalahHewan.Wadah<Hewan>cocok denganWadah<in Hewan>.tujuan.ambil().nama: setelahsalinDariKe,tujuanberisiKucing("Tom"). SebelumnyatujuanberisiAnjing("Placeholder"), tetapisalinDariKemenimpa isinya denganKucing("Tom")dari sumber.
Perbandingan dengan Java Wildcard #
Java memakai wildcard ? extends T dan ? super T untuk use-site variance. Konsepnya identik — ? extends T setara dengan out T, dan ? super T setara dengan in T. Sintaksis Kotlin lebih ringkas dan tidak memerlukan tanda tanya.
// Java: void proses(List<? extends Number> daftar)
// Kotlin: fun proses(daftar: List<out Number>)
// Java: void isi(List<? super Integer> daftar)
// Kotlin: fun isi(daftar: List<in Int>)Kapan Memakai Use-Site Variance #
Use-site variance cocok ketika class invariant tetapi pada konteks tertentu hanya membaca atau hanya menulis. Misalnya, Array<T> bersifat invariant (karena dapat membaca dan menulis), tetapi pada function yang hanya membaca elemen, kita dapat memakai Array<out T>. Sebaliknya, declaration-site variance lebih cocok ketika behavior class secara alami hanya satu arah — seperti List<out E> yang tidak dapat menulis.
Referensi Lanjutan #
- Generics: Variance
— penjelasan resmi use-site variance dengan type projection
out/in
Kombinasi keduanya memberi Kotlin fleksibilitas penuh. Pada chapter Star Projections kita akan melihat bentuk variance khusus * yang memungkinkan kita bekerja dengan tipe generic tanpa peduli terhadap type argument.