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:

kotlin Use-site 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:

ProjectionArtiPadanan Java
Wadah<out T>Hanya membaca T dari wadahWadah<? extends T>
Wadah<in T>Hanya menulis T ke wadahWadah<? 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:

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

text
Isi: Miko
Isi: Rex
Setelah isiDenganHewan: Default
Sumber setelah salin: Tom
Tujuan setelah salin: Tom

Mari kita telaah baris-baris kode di atas:

  1. class Wadah<T>(var isi: T): class invariant. T muncul di posisi var (in & out). Tidak dapat dideklarasikan <out T> atau <in T>.
  2. fun lihatIsi(wadah: Wadah<out Hewan>): use-site out projection. Function hanya memanggil wadah.ambil() — tidak memanggil taruh. Karena variance ditentukan pada signature, pemanggil dapat meneruskan Wadah<Kucing> atau Wadah<Anjing>.
  3. fun isiDenganHewan(wadah: Wadah<in Hewan>): use-site in projection. Function hanya memanggil wadah.taruh(Kucing(...)). Pemanggil dapat meneruskan Wadah<Hewan> atau Wadah<Any>.
  4. val terisi = isiDenganHewan(wadahHewanKosong): pemanggilan valid. Wadah<Hewan> cocok dengan parameter Wadah<in Hewan>.
  5. @Suppress("UNCHECKED_CAST"): cast Wadah<Hewan> dari Wadah<in Hewan> memerlukan penenangan peringatan kompilator. Cast ini aman karena kita baru saja menulis Kucing ke wadah, sehingga isinya pasti Hewan.
  6. fun salinDariKe(dari: Wadah<out Hewan>, ke: Wadah<in Hewan>): kombinasi out dan in projection. dari hanya dibaca, ke hanya ditulis. Function ini bekerja untuk Wadah<Kucing> sebagai sumber dan Wadah<Hewan> sebagai tujuan.
  7. salinDariKe(sumber, tujuan): pemanggilan valid. Wadah<Kucing> cocok dengan Wadah<out Hewan> karena Kucing adalah Hewan. Wadah<Hewan> cocok dengan Wadah<in Hewan>.
  8. tujuan.ambil().nama: setelah salinDariKe, tujuan berisi Kucing("Tom"). Sebelumnya tujuan berisi Anjing("Placeholder"), tetapi salinDariKe menimpa isinya dengan Kucing("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.

kotlin Padanan Java wildcard
// 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 #

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.