Saat kita menulis class Box<T>, type parameter T tidak memiliki batasan — semua tipe boleh dimasukkan. Pada banyak kasus, kita ingin membatasi tipe yang boleh dipakai sebagai type argument. Misalnya, sebuah class yang menghitung operasi matematika hanya boleh menerima Number dan turunannya. Batasan ini disebut upper bound dan ditulis dengan tanda titik dua. Pada chapter ini kita akan mempelajari bound tunggal, multiple bounds dengan klausa where, serta melihat bagaimana kombinasi bound memungkinkan kita menulis kode generic yang tetap kaya fitur.

Bound Tunggal #

Bound tunggal membatasi type parameter ke satu tipe saja. Sintaksisnya adalah <T : BoundType>. Setelah bound dipasang, semua method yang dimiliki BoundType dapat dipanggil di dalam body class atau function generic.

kotlin Bound tunggal
class NumberBox<T : Number>(val value: T) {
    fun doubled(): Double = value.toDouble() * 2
}

Karena T dibatasi menjadi Number, method toDouble tersedia. Tanpa bound, kompilator menolak pemanggilan tersebut karena T dianggap sebagai Any?.

Multiple Bounds dengan where #

Kadang satu bound tidak cukup. Sebagai contoh, kita ingin type parameter T memenuhi dua kondisi sekaligus — Number dan Comparable<T>. Untuk kasus ini, Kotlin menyediakan klausa where yang ditaruh setelah deklarasi parameter constructor atau sebelum body function.

kotlin Multiple bounds dengan where
class ItemRingkas<T>(val data: T) where T : Number, T : DapatDijelaskan {
    fun ringkas(): String = "Nilai ${data} (${data.jelaskan()})"
}

Klausa where T : Number, T : DapatDijelaskan memaksa T harus subtype dari Number sekaligus mengimplementasikan DapatDijelaskan. Hal ini memungkinkan kita memanggil data.toDouble() (dari Number) dan data.jelaskan() (dari DapatDijelaskan) di dalam body class.

Contoh Lengkap dengan Class Kustom #

Untuk mendemonstrasikan multiple bound, kita perlu class yang memenuhi semua kondisi. Pada contoh berikut, kita membuat class MyNumber yang meng-extend Number sekaligus mengimplementasikan Comparable<MyNumber> dan DapatDijelaskan:

kotlin
// Chapter: Upper Bounds
// Membatasi type parameter dengan upper bound tunggal dan jamak.

class NumberBox<T : Number>(val value: T) {
    fun doubled(): Double = value.toDouble() * 2
    fun isGreaterThan(other: Number): Boolean = value.toDouble() > other.toDouble()
}

interface DapatDijelaskan {
    fun jelaskan(): String
}

class ItemRingkas<T>(val data: T) where T : Number, T : DapatDijelaskan {
    fun ringkas(): String = "Nilai ${data} (${data.jelaskan()})"
}

class MetricNumber<T>(val nilai: T) : DapatDijelaskan where T : Number, T : Comparable<T> {
    override fun jelaskan(): String = "angka $nilai"
    fun bandingkanDengan(lain: T): Int = nilai.compareTo(lain)
}

// Bound dengan tipe parameter lain
class KomparatorBox<T : Comparable<T>>(val a: T, val b: T) {
    fun yangTerbesar(): T = if (a >= b) a else b
}

// Fungsi generik dengan multiple where
fun <T> jumlahkan(a: T, b: T): Double where T : Number, T : Comparable<T> {
    return if (a >= b) a.toDouble() + b.toDouble() else b.toDouble() + a.toDouble()
}

@Suppress("DEPRECATION", "OverridingDeprecatedMember")
class MyNumber(val value: Int) : Number(), Comparable<MyNumber>, DapatDijelaskan {
    override fun toDouble(): Double = value.toDouble()
    override fun toFloat(): Float = value.toFloat()
    override fun toLong(): Long = value.toLong()
    override fun toInt(): Int = value
    override fun toChar(): Char = value.toChar()
    override fun toByte(): Byte = value.toByte()
    override fun toShort(): Short = value.toShort()
    override fun compareTo(other: MyNumber): Int = value.compareTo(other.value)
    override fun jelaskan(): String = "MyNumber($value)"
    override fun toString(): String = "MyNumber($value)"
}

fun main() {
    val kotak1 = NumberBox(10)
    println("Doubled: ${kotak1.doubled()}")
    println("Lebih besar dari 5? ${kotak1.isGreaterThan(5)}")

    val kotak2 = NumberBox(3.14)
    println("Doubled (double): ${kotak2.doubled()}")

    val cb = KomparatorBox("Mangga", "Apel")
    println("Terbesar: ${cb.yangTerbesar()}")

    val cbInt = KomparatorBox(42, 17)
    println("Terbesar (int): ${cbInt.yangTerbesar()}")

    val jumlah = jumlahkan(MyNumber(5), MyNumber(8))
    println("Jumlahkan: $jumlah")

    val m = MetricNumber(MyNumber(7))
    val banding = m.bandingkanDengan(MyNumber(3))
    println("Bandingkan MyNumber(7) vs MyNumber(3): $banding")

    val item = ItemRingkas(MyNumber(15))
    println(item.ringkas())
}

Output yang Dihasilkan:

text
Doubled: 20.0
Lebih besar dari 5? true
Doubled (double): 6.28
Terbesar: Mangga
Terbesar (int): 42
Jumlahkan: 13.0
Bandingkan MyNumber(7) vs MyNumber(3): 1
Nilai MyNumber(15) (MyNumber(15))

Mari kita telaah baris-baris kode di atas:

  1. class NumberBox<T : Number>: bound tunggal pada level deklarasi class. T harus subtype Number.
  2. class ItemRingkas<T>(val data: T) where T : Number, T : DapatDijelaskan: multiple bounds dengan where. Koma memisahkan setiap bound. T harus memenuhi keduanya.
  3. class MetricNumber<T>(val nilai: T) : DapatDijelaskan where T : Number, T : Comparable<T>: class generic yang juga mengimplementasikan interface. Bound where ditaruh setelah deklarasi constructor dan sebelum body class.
  4. class KomparatorBox<T : Comparable<T>>: bound tunggal yang merupakan recursive bound — T dibandingkan dengan dirinya sendiri.
  5. fun <T> jumlahkan(a: T, b: T): Double where T : Number, T : Comparable<T>: function generic dengan multiple bounds. Di dalam body, kita dapat memanggil a.toDouble() dan membandingkan a >= b karena kedua method tersedia dari bound masing-masing.
  6. @Suppress("DEPRECATION", "OverridingDeprecatedMember"): anotasi untuk menenangkan peringatan kompilator terkait method toChar yang ditandai deprecated pada class Number.
  7. class MyNumber : Number(), Comparable<MyNumber>, DapatDijelaskan: class kustom yang memenuhi semua bound. Wajib meng-override method abstrak Number (toInt, toDouble, dan seterusnya).
  8. ItemRingkas(MyNumber(15)): karena MyNumber memenuhi Number dan DapatDijelaskan, kompilator menerima pemanggilan. Method ringkas memanggil data.toDouble() dan data.jelaskan() — keduanya tersedia karena multiple bound.

Kapan Memakai Multiple Bounds #

Multiple bounds cocok ketika kita menulis function generic yang membutuhkan beberapa perilaku sekaligus. Misalnya, function yang membandingkan dan menjumlahkan harus memastikan type argument adalah Comparable sekaligus Number. Namun, semakin banyak bound, semakin sempit set tipe yang memenuhi syarat — pertimbangkan apakah class biasa dengan antarmuka yang sama lebih sederhana.

Referensi Lanjutan #

Bound memengaruhi fleksibilitas generic, namun ada topik terpisah yang berkaitan langsung dengan subtype relationship: variance. Pada chapter Variance kita akan melihat mengapa List<String> bukan subtype List<Any> meskipun String adalah subtype Any.