Setelah memahami covariance dan contravariance, kita perlu memilih di mana variance dideklarasikan. Kotlin menyediakan dua lokasi: declaration-site dan use-site. Declaration-site variance berarti variance ditentukan sekali pada saat class dideklarasikan, dan setiap pemakaian class tersebut secara otomatis mengikuti aturan variance tersebut. Pendekatan ini lebih ringkas dibanding Java yang memakai use-site variance dengan wildcard ? extends dan ? super. Pada chapter ini kita akan membandingkan kedua pendekatan dan melihat mengapa declaration-site variance membuat kode Kotlin lebih bersih.
Apa Itu Declaration-Site Variance #
Declaration-site variance berarti keyword out atau in ditulis pada deklarasi class itu sendiri, bukan pada lokasi pemakaian. Setelah deklarasi, semua referensi ke class tersebut mengikuti variance yang sudah ditentukan — pemakai tidak perlu mengulanginya.
class Kandang<out T : Hewan>(private val penghuni: T) {
fun lihat(): T = penghuni
}
class Pelihara<in T : Hewan> {
fun beriMakan(hewan: T): String = "Memberi makan ke ${hewan.nama}"
}Pada contoh di atas, Kandang bersifat covariant dan Pelihara bersifat contravariant. Setiap kali kita memakai Kandang atau Pelihara, aturan variance sudah berlaku tanpa kita menulis out atau in lagi.
Keuntungan Declaration-Site Variance #
Keuntungan utama declaration-site variance adalah keringkasan. Pemakai class tidak perlu tahu detail variance — cukup tahu bahwa Kandang<Kucing> adalah subtype Kandang<Hewan>. Hal ini membuat signature function lebih bersih dan mengurangi beban kognitif.
fun lihatSemua(kandang: Kandang<Hewan>) {
println(kandang.lihat().nama)
}
// Pemanggil dapat meneruskan Kandang<Kucing> tanpa perlu out eksplisit
lihatSemua(Kandang(Kucing("Miko")))Contoh Lengkap dengan Hierarki Hewan #
Mari kita lihat bagaimana declaration-site variance memengkinkan kode yang fleksibel dan ringkas:
// Chapter: Declaration-Site Variance
// Variance ditentukan saat deklarasi class/interface: <out T> atau <in T>.
interface Hewan {
val nama: String
}
class Kucing(override val nama: String) : Hewan
class Anjing(override val nama: String) : Hewan
// Declaration-site covariance: <out T>
// Hanya boleh memproduksi T (return), tidak boleh mengkonsumsi.
class Kandang<out T : Hewan>(private val penghuni: T) {
fun lihat(): T = penghuni
}
// Declaration-site contravariance: <in T>
// Hanya boleh mengkonsumsi T (parameter), tidak boleh menghasilkan.
class Pelihara<in T : Hewan> {
fun beriMakan(hewan: T): String = "Memberi makan ke ${hewan.nama}"
}
// Standard library: List<out E>
fun totalNamaHewan(daftar: List<Hewan>): Int {
var total = 0
for (h in daftar) total += h.nama.length
return total
}
fun main() {
val kandangKucing: Kandang<Kucing> = Kandang(Kucing("Miko"))
val kandangHewan: Kandang<Hewan> = kandangKucing
println("Penghuni kandang: ${kandangHewan.lihat().nama}")
val kandangAnjing: Kandang<Anjing> = Kandang(Anjing("Rex"))
val kandangHewan2: Kandang<Hewan> = kandangAnjing
println("Penghuni kandang: ${kandangHewan2.lihat().nama}")
val peliharaHewan = Pelihara<Hewan>()
val peliharaKucing: Pelihara<Kucing> = peliharaHewan
println(peliharaKucing.beriMakan(Kucing("Tom")))
val daftarKucing: List<Kucing> = listOf(Kucing("A"), Kucing("Boo"))
val daftarHewan: List<Hewan> = daftarKucing
println("Total huruf nama hewan: ${totalNamaHewan(daftarHewan)}")
println("Total huruf nama kucing: ${totalNamaHewan(daftarKucing)}")
}Output yang Dihasilkan:
Penghuni kandang: Miko
Penghuni kandang: Rex
Memberi makan ke Tom
Total huruf nama hewan: 4
Total huruf nama kucing: 4Mari kita telaah baris-baris kode di atas:
class Kandang<out T : Hewan>(private val penghuni: T): declaration-site variance denganout.Thanya muncul sebagai tipe kembalianlihat(). Variance berlaku untuk semua pemakaianKandangselanjutnya.val kandangHewan: Kandang<Hewan> = kandangKucing: assignment valid tanpa perlu menulisoutlagi. Variance sudah ditentukan saat deklarasi class.class Pelihara<in T : Hewan>: declaration-site variance denganin.Thanya muncul sebagai tipe parameter methodberiMakan. Contravariance berlaku untuk semua pemakaian.val peliharaKucing: Pelihara<Kucing> = peliharaHewan: assignment valid tanpa perlu menulisinlagi.Pelihara<Hewan>adalah subtypePelihara<Kucing>.fun totalNamaHewan(daftar: List<Hewan>): function menerimaList<Hewan>. KarenaList<out E>di standard library, kita dapat meneruskanList<Kucing>.totalNamaHewan(daftarKucing): pemanggilan valid.List<Kucing>adalah subtypeList<Hewan>karenaListbersifat covariant. Function membaca setiap elemen sebagaiHewan, dan methodh.nama.lengthdapat dipanggil karenaHewanmemiliki propertinama.
Declaration-Site vs Use-Site #
Declaration-site variance adalah pilihan Kotlin, sedangkan Java memakai use-site variance dengan wildcard ? extends T dan ? super T. Berikut perbandingannya:
| Aspek | Declaration-Site (Kotlin) | Use-Site (Java) |
|---|---|---|
| Lokasi variance | Deklarasi class | Setiap pemakaian tipe |
| Mengulang variance | Tidak perlu | Setiap signature |
| Sintaksis | <out T> / <in T> | <? extends T> / <? super T> |
| Risiko lupa | Rendah — ditulis sekali | Tinggi — bisa lupa di setiap signature |
| Fleksibilitas | Variance tetap untuk semua pemakaian | Variance dapat berbeda per pemakaian |
Declaration-site variance cocok ketika behavior class secara alami hanya memproduksi atau hanya mengkonsumsi T. Use-site variance masih berguna ketika class sebenarnya invariant, tetapi pada pemakaian tertentu kita hanya akan membaca atau hanya menulis.
Kapan Declaration-Site Tidak Cocok #
Declaration-site variance tidak cocok ketika class sebenarnya dapat memproduksi dan mengkonsumsi T, tetapi pada konteks pemakaian tertentu hanya salah satu yang dibutuhkan. Untuk kasus seperti ini, kita membiarkan class invariant dan memakai use-site variance dengan out atau in pada lokasi pemakaian.
Referensi Lanjutan #
- Generics: Variance
— penjelasan resmi declaration-site variance dengan
outdanin
Pada chapter Use-Site Variance kita akan melihat bagaimana Kotlin mengakomodasi kasus tersebut dengan out dan in pada signature function atau variabel.