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.

kotlin Deklarasi contravariant
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 TBoleh pada in T?
Tipe parameter methodYa
Tipe kembalian methodTidak
Tipe properti valTidak
Tipe properti varTidak

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:

kotlin
// 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:

text
Banding Miko vs Tom: -7
Banding Rex vs Buddy: 16
Terima hewan: Miko
Mencatat hewan bernama Tom

Mari kita telaah baris-baris kode di atas:

  1. interface Banding<in T>: type parameter T dipakai hanya sebagai tipe parameter method bandingkan. Karena itu, aman dideklarasikan sebagai in T.
  2. class BandingHewanNama : Banding<Hewan>: implementasi yang membandingkan dua Hewan berdasarkan nama. Implementasi ini tidak peduli apakah argumen sebenarnya Kucing atau Anjing — semuanya Hewan.
  3. val bandingKucing: Banding<Kucing> = bandingHewan: contravariance memungkinkan assignment ini. Banding<Hewan> adalah subtype Banding<Kucing> karena contravariance membalik arah subtype.
  4. bandingKucing.bandingkan(k1, k2): kompilator menerima pemanggilan dengan argumen Kucing. Saat runtime, method memperlakukan argumen sebagai Hewan — aman karena Kucing adalah Hewan.
  5. class Konsumen<in T>(private val aksi: (T) -> Unit): type parameter T hanya muncul di posisi in. Function type (T) -> Unit memperlakukan T sebagai parameter input sehingga T berada di posisi in — aman untuk in T.
  6. val konsumenKucing: Konsumen<Kucing> = konsumenHewan: assignment valid. Konsumen<Hewan> adalah subtype Konsumen<Kucing>. Lambda yang menerima Hewan dapat menerima Kucing.
  7. class PengelolaHewan<in T : Hewan>: contravariance dapat dikombinasikan dengan bound. T harus subtype Hewan dan hanya boleh muncul di posisi in.
  8. val pengelolaKucing: PengelolaHewan<Kucing> = pengelolaHewan: PengelolaHewan<Hewan> adalah subtype PengelolaHewan<Kucing>. Method catat menerima Kucing, dan implementasi memperlakukannya sebagai Hewan.

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 #

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.