Covariance menangani class yang memproduksi nilai T. Bagaimana dengan class yang mengkonsumsi nilai T? Untuk class seperti ini, hubungan subtype berjalan terbalik — Consumer<Hewan> adalah subtype Consumer<Kucing>, karena Consumer<Hewan> dapat menangani setiap Kucing (atau Anjing, atau Hewan apa pun). Konsep ini disebut contravariance dan ditandai dengan keyword in pada deklarasi type parameter. Pada chapter ini kita akan memahami contravariance, melihat aturan posisi yang dipaksakannya, serta mempelajari mengapa Comparator di standard library bersifat contravariant.
Konsep Contravariance #
Contravariance berarti: jika A adalah subtype B, maka Consumer<B> adalah subtype Consumer<A>. Perhatikan arah yang berlawanan dari covariance. Aturan ini aman ketika class hanya mengkonsumsi T — tidak pernah memproduksinya. Kotlin memakai keyword in pada deklarasi type parameter.
interface Banding<in T> {
fun bandingkan(a: T, b: T): Int
}Keyword in T memberitahu kompilator bahwa T hanya akan muncul di posisi in — yaitu sebagai tipe parameter method, bukan sebagai tipe kembalian. Karena T tidak pernah diproduksi, aman untuk memperlakukan Banding<Hewan> sebagai Banding<Kucing>.
Aturan Posisi in #
Kompilator memverifikasi setiap kemunculan T berada di posisi in. Jika T muncul sebagai tipe kembalian method, kompilator menolak deklarasi in T.
Posisi T | Boleh pada in T? |
|---|---|
| Tipe parameter method | Ya |
| Tipe kembalian method | Tidak |
Tipe properti val | Tidak |
Tipe properti var | Tidak |
Contoh Lengkap dengan Banding dan Konsumen #
Mari kita lihat contravariance dalam aksi dengan class BandingHewanNama yang membandingkan dua Hewan berdasarkan nama. Karena Banding bersifat contravariant, Banding<Hewan> dapat dipakai di mana Banding<Kucing> atau Banding<Anjing> dibutuhkan:
// Chapter: Contravariance
// Consumer<in T>: hanya boleh mengkonsumsi (parameter), tidak boleh memproduksi.
interface Hewan {
val nama: String
}
class Kucing(override val nama: String) : Hewan
class Anjing(override val nama: String) : Hewan
// Banding<in T>: hanya menerima T sebagai parameter, tidak mengembalikan T.
interface Banding<in T> {
fun bandingkan(a: T, b: T): Int
}
class BandingHewanNama : Banding<Hewan> {
override fun bandingkan(a: Hewan, b: Hewan): Int = a.nama.compareTo(b.nama)
}
class Konsumen<in T>(private val aksi: (T) -> Unit) {
fun terima(item: T) {
aksi(item)
}
}
class PengelolaHewan<in T : Hewan> {
fun catat(hewan: T): String = "Mencatat hewan bernama ${hewan.nama}"
}
fun main() {
val bandingHewan: Banding<Hewan> = BandingHewanNama()
// Kontravarian: Banding<Hewan> adalah subtype Banding<Kucing>
val bandingKucing: Banding<Kucing> = bandingHewan
val k1 = Kucing("Miko")
val k2 = Kucing("Tom")
println("Banding ${k1.nama} vs ${k2.nama}: ${bandingKucing.bandingkan(k1, k2)}")
// Banding<Hewan> juga subtype Banding<Anjing>
val bandingAnjing: Banding<Anjing> = bandingHewan
val a1 = Anjing("Rex")
val a2 = Anjing("Buddy")
println("Banding ${a1.nama} vs ${a2.nama}: ${bandingAnjing.bandingkan(a1, a2)}")
val konsumenHewan = Konsumen<Hewan> { println("Terima hewan: ${it.nama}") }
val konsumenKucing: Konsumen<Kucing> = konsumenHewan
konsumenKucing.terima(Kucing("Miko"))
val pengelolaHewan = PengelolaHewan<Hewan>()
val pengelolaKucing: PengelolaHewan<Kucing> = pengelolaHewan
println(pengelolaKucing.catat(Kucing("Tom")))
}Output yang Dihasilkan:
Banding Miko vs Tom: -7
Banding Rex vs Buddy: 16
Terima hewan: Miko
Mencatat hewan bernama TomMari kita telaah baris-baris kode di atas:
interface Banding<in T>: type parameterTdipakai hanya sebagai tipe parameter methodbandingkan. Karena itu, aman dideklarasikan sebagaiin T.class BandingHewanNama : Banding<Hewan>: implementasi yang membandingkan duaHewanberdasarkan nama. Implementasi ini tidak peduli apakah argumen sebenarnyaKucingatauAnjing— semuanyaHewan.val bandingKucing: Banding<Kucing> = bandingHewan: contravariance memungkinkan assignment ini.Banding<Hewan>adalah subtypeBanding<Kucing>karena contravariance membalik arah subtype.bandingKucing.bandingkan(k1, k2): kompilator menerima pemanggilan dengan argumenKucing. Saat runtime, method memperlakukan argumen sebagaiHewan— aman karenaKucingadalahHewan.class Konsumen<in T>(private val aksi: (T) -> Unit): type parameterThanya muncul di posisi in. Function type(T) -> UnitmemperlakukanTsebagai parameter input sehinggaTberada di posisi in — aman untukin T.val konsumenKucing: Konsumen<Kucing> = konsumenHewan: assignment valid.Konsumen<Hewan>adalah subtypeKonsumen<Kucing>. Lambda yang menerimaHewandapat menerimaKucing.class PengelolaHewan<in T : Hewan>: contravariance dapat dikombinasikan dengan bound.Tharus subtypeHewandan hanya boleh muncul di posisi in.val pengelolaKucing: PengelolaHewan<Kucing> = pengelolaHewan:PengelolaHewan<Hewan>adalah subtypePengelolaHewan<Kucing>. MethodcatatmenerimaKucing, dan implementasi memperlakukannya sebagaiHewan.
Mengapa Contravariance Aman #
Contravariance aman ketika class hanya mengkonsumsi T. Pemanggil meneruskan nilai yang lebih sempit (misalnya Kucing), dan implementasi yang sebenarnya menangani tipe yang lebih luas (Hewan). Karena setiap Kucing adalah Hewan, tidak ada kemungkinan ClassCastException di runtime.
Kapan Memakai Contravariance #
Contravariance cocok untuk class yang berperan sebagai consumer — class yang menerima T untuk diproses. Contoh klasik adalah Comparator, Consumer, atau Handler. Sebaliknya, class yang memproduksi T harus covariant dengan out.
Referensi Lanjutan #
- Generics: Variance
— bagian contravariance (
in T) pada panduan variance resmi
Kombinasi out untuk producer dan in untuk consumer dikenal sebagai prinsip PECS (Producer Extends, Consumer Super) di Java. Di Kotlin, prinsip ini diterjemahkan menjadi out dan in. Pada chapter Declaration-Site Variance kita akan melihat bagaimana variance ditentukan sekali pada deklarasi class sehingga pemakai tidak perlu mengulanginya.