Pada chapter sebelumnya kita melihat bahwa Kotlin memperlakukan class generic secara invariant secara default. Namun, ada situasi di mana invariant terlalu membatasi. Bayangkan sebuah class Producer<T> yang hanya memproduksi nilai T — tidak pernah mengkonsumsinya. Untuk class seperti ini, aman untuk mengatakan bahwa Producer<String> adalah subtype Producer<Any>, karena pemanggil hanya akan membaca nilai dan setiap String memang Any. Pada chapter ini kita akan membedah covariance dengan keyword out, melihat batasan apa yang dipaksakannya pada method class, serta memahami mengapa List di standard library bersifat covariant.
Konsep Covariance #
Covariance berarti: jika A adalah subtype B, maka Producer<A> adalah subtype Producer<B>. Aturan ini aman hanya ketika class Producer tidak pernah mengkonsumsi T — hanya memproduksinya. Kotlin memakai keyword out pada deklarasi type parameter untuk menandai covariance.
class Producer<out T>(private val sumber: () -> T) {
fun ambil(): T = sumber()
}Keyword out T memberitahu kompilator bahwa T hanya akan muncul di posisi out — yaitu sebagai tipe kembalian method, bukan sebagai tipe parameter. Karena T tidak pernah dikonsumsi, aman untuk memperlakukan Producer<String> sebagai Producer<Any>.
Aturan Posisi out #
Kompilator memverifikasi bahwa setiap kemunculan T di dalam class berada di posisi out. Jika kita mencoba menambahkan method yang menerima T sebagai parameter, kompilator menolak. Aturan ini disebut declaration-site variance — variance ditentukan pada saat deklarasi class.
Posisi T | Boleh pada out T? |
|---|---|
| Tipe kembalian method | Ya |
| Tipe parameter method | Tidak |
Tipe properti val | Ya |
Tipe properti var | Tidak |
Contoh Lengkap dengan Class Kucing #
Mari kita lihat covariance dalam aksi dengan hierarki Hewan → Kucing/Anjing. Kita akan membuat Producer<out T> dan Kandang<out T : Hewan>:
// Chapter: Covariance
// Producer<out T>: hanya boleh memproduksi (return), tidak boleh mengkonsumsi.
interface Hewan {
val nama: String
fun suara(): String
}
class Kucing(override val nama: String) : Hewan {
override fun suara(): String = "Meong"
}
class Anjing(override val nama: String) : Hewan {
override fun suara(): String = "Guk"
}
class Producer<out T>(private val sumber: () -> T) {
fun ambil(): T = sumber()
}
class Kandang<out T : Hewan>(private val penghuni: T) {
fun lihatPenghuni(): T = penghuni
}
fun main() {
val produsenKucing: Producer<Kucing> = Producer { Kucing("Miko") }
val produsenHewan: Producer<Hewan> = produsenKucing
val hewanDariProdusen: Hewan = produsenHewan.ambil()
println("Hewan dari produsen: ${hewanDariProdusen.nama} bersuara ${hewanDariProdusen.suara()}")
val kandangKucing: Kandang<Kucing> = Kandang(Kucing("Tom"))
val kandangHewan: Kandang<Hewan> = kandangKucing
val penghuni: Hewan = kandangHewan.lihatPenghuni()
println("Penghuni kandang: ${penghuni.nama}")
val daftarKucing: List<Kucing> = listOf(Kucing("A"), Kucing("B"))
val daftarHewan: List<Hewan> = daftarKucing
println("Jumlah hewan: ${daftarHewan.size}")
for (h in daftarHewan) {
println("- ${h.nama}: ${h.suara()}")
}
val kandangAnjing: Kandang<Anjing> = Kandang(Anjing("Rex"))
val kandangHewan2: Kandang<Hewan> = kandangAnjing
println("Penghuni anjing: ${kandangHewan2.lihatPenghuni().nama}")
}Output yang Dihasilkan:
Hewan dari produsen: Miko bersuara Meong
Penghuni kandang: Tom
Jumlah hewan: 2
- A: Meong
- B: Meong
Penghuni anjing: RexMari kita telaah baris-baris kode di atas:
interface Hewandanclass Kucing : Hewan: hierarki tipe yang akan kita pakai.KucingdanAnjingadalah subtypeHewan.class Producer<out T>(private val sumber: () -> T): type parameterTdipakai hanya sebagai tipe kembalian methodambil. Karena itu, aman dideklarasikan sebagaiout T.val produsenHewan: Producer<Hewan> = produsenKucing: covariance memungkinkan assignment ini.Producer<Kucing>adalah subtypeProducer<Hewan>.produsenHewan.ambil(): Hewan: kompilator menganggap tipe kembalian sebagaiHewan, bukanKucing. Hal ini aman karena setiapKucingadalahHewan.class Kandang<out T : Hewan>: covariance dapat dikombinasikan dengan bound.Tharus subtypeHewansekaligus berada di posisi out.val kandangHewan: Kandang<Hewan> = kandangKucing: assignment ini valid karenaKandangbersifat covariant. Pembaca yang menerimaKandang<Hewan>dapat menerimaKandang<Kucing>atauKandang<Anjing>.val daftarHewan: List<Hewan> = daftarKucing:Listdi standard library dideklarasikan sebagaiList<out E>. Inilah sebabnyaList<String>atauList<Kucing>bisa diteruskan ke function yang menerimaList<Any>atauList<Hewan>.for (h in daftarHewan): iterasi aman. Setiap elemen diperlakukan sebagaiHewan, dan pemanggilanh.suara()di-resolve secara polimorfik melalui dynamic dispatch.
Mengapa Covariance Aman #
Covariance aman ketika class hanya memproduksi T. Pemanggil hanya membaca nilai, dan setiap nilai T adalah SuperType. Sebaliknya, jika class mengkonsumsi T (menerima T sebagai parameter), covariance akan berbahaya karena pemanggil dapat meneruskan tipe yang lebih luas dari yang seharusnya diterima class. Inilah alasan kompilator melarang T muncul di posisi in ketika out T dideklarasikan.
Kapan Memakai Covariance #
Covariance cocok untuk class yang berperan sebagai producer atau source — class yang hanya mengembalikan nilai T. Contoh klasik adalah Iterator, List, atau Supplier. Sebaliknya, class yang menerima T untuk diproses (seperti Comparator) tidak boleh covariant — topik yang akan kita bahas pada chapter Contravariance.
Referensi Lanjutan #
- Generics: Variance
— bagian covariance (
out T) pada panduan variance resmi
Covariance yang kita pakai di sini disebut declaration-site variance karena ditentukan pada saat deklarasi class. Pada chapter Declaration-Site Variance kita akan membandingkan pendekatan ini dengan use-site variance yang dipakai di Java.