Type parameter adalah inti dari generics. Type parameter berfungsi sebagai placeholder tipe yang akan diisi kemudian, baik saat instantiasi class generic maupun saat memanggil function generic. Pemilihan nama type parameter memengaruhi keterbacaan kode secara signifikan — kompilator menerima nama apa pun, namun komunitas Kotlin memakai konvensi huruf kapital tunggal yang memiliki makna tertentu. Pada chapter ini kita akan memetakan konvensi penamaan type parameter, memahami kapan menggunakan bound tunggal maupun recursive bound, serta melihat bagaimana bound memengaruhi method yang dapat dipanggil di dalam class atau function generic.
Konvensi Penamaan #
Komunitas Kotlin mewarisi konvensi dari Java: nama type parameter memakai huruf kapital tunggal dengan makna khusus. Berikut pemetaan konvensi yang umum dipakai:
| Nama | Makna Umum | Contoh Penggunaan |
|---|---|---|
T | Type (umum) | class Box<T> |
E | Element (koleksi) | class Antrian<E> |
K | Key (map) | class Peta<K, V> |
V | Value (map) | class Peta<K, V> |
R | Return (function) | fun <R> map(f: () -> R) |
T1, T2 | Beberapa tipe umum | fun <T1, T2> gabung(a: T1, b: T2) |
Konvensi bukan aturan wajib. Kompilator menerima nama yang lebih deskriptif seperti class Repository<EntityType>. Namun, nama deskriptif jarang dipakai karena biasanya type parameter muncul di banyak tempat dan nama panjang membuat signature menjadi padat.
Bound Tunggal #
Tanpa bound, type parameter hanya bisa diperlakukan sebagai Any?. Artinya, kita tidak bisa memanggil method khusus tipe tertentu. Bound membatasi type parameter ke tipe tertentu, sehingga method pada tipe tersebut dapat dipakai di dalam body. Bound ditulis dengan tanda titik dua setelah nama type parameter.
class PenampungAngka<T : Number>(val nilai: T) {
fun doubleNilai(): Double = nilai.toDouble() * 2
}Pada contoh di atas, T : Number berarti T harus subtype dari Number. Dengan batasan ini, kita dapat memanggil nilai.toDouble() karena Number memiliki method toDouble. Tanpa bound, kompilator tidak akan mengizinkan pemanggilan method tersebut.
Recursive Bound #
Recursive bound adalah bound yang merujuk pada type parameter itu sendiri. Pola paling umum adalah <T : Comparable<T>>, yang berarti T harus dapat dibandingkan dengan dirinya sendiri. Hal ini memungkinkan kita menulis function umum yang bekerja pada tipe apa pun selama tipe tersebut dapat dibandingkan.
// Chapter: Type Parameters
// Konvensi penamaan dan batasan type parameter.
// T: Type umum
class Kotak<T>(val isi: T) {
fun ambil(): T = isi
}
// E: Element (untuk koleksi)
class Antrian<E> {
private val item = mutableListOf<E>()
fun masuk(e: E) = item.add(e)
fun keluar(): E? = if (item.isEmpty()) null else item.removeAt(0)
fun ukuran(): Int = item.size
}
// K, V: Key, Value
class PetaSederhana<K, V> {
private val pasangan = mutableListOf<Pair<K, V>>()
fun taruh(k: K, v: V) {
pasangan.add(Pair(k, v))
}
fun ambil(k: K): V? = pasangan.firstOrNull { it.first == k }?.second
}
// R: Return type untuk function
fun <R> jalankan(pemasok: () -> R): R = pemasok()
// T dengan bound tunggal
class PenampungAngka<T : Number>(val nilai: T) {
fun doubleNilai(): Double = nilai.toDouble() * 2
}
// T dengan recursive bound (Comparable<T>)
fun <T : Comparable<T>> urutkanDanAmbilMaks(a: T, b: T, c: T): T {
val daftar = listOf(a, b, c).sorted()
return daftar.last()
}
fun main() {
val kotakStr = Kotak("Halo")
println("Isi: ${kotakStr.ambil()}")
val antrian = Antrian<String>()
antrian.masuk("Budi")
antrian.masuk("Siti")
println("Keluar: ${antrian.keluar()}")
println("Sisa: ${antrian.ukuran()}")
val peta = PetaSederhana<String, Int>()
peta.taruh("satu", 1)
peta.taruh("dua", 2)
println("Peta['dua'] = ${peta.ambil("dua")}")
val hasil = jalankan { 40 + 2 }
println("Hasil jalankan: $hasil")
val penampung = PenampungAngka(10)
println("Double: ${penampung.doubleNilai()}")
val maks = urutkanDanAmbilMaks(3, 9, 5)
println("Maks dari (3, 9, 5): $maks")
val maksStr = urutkanDanAmbilMaks("Mangga", "Apel", "Pisang")
println("Maks dari (Mangga, Apel, Pisang): $maksStr")
}Output yang Dihasilkan:
Isi: Halo
Keluar: Budi
Sisa: 1
Peta['dua'] = 2
Hasil jalankan: 42
Double: 20.0
Maks dari (3, 9, 5): 9
Maks dari (Mangga, Apel, Pisang): PisangMari kita telaah baris-baris kode di atas:
class Kotak<T>(val isi: T): type parameterTtanpa bound. Kita bisa memasukkan tipe apa pun, namun tidak bisa memanggil method khusus sepertitoDouble.class Antrian<E>: konvensiEuntuk elemen koleksi. Methodmasuk,keluar, danukuranbekerja pada tipeE.class PetaSederhana<K, V>: dua type parameter,Kuntuk kunci danVuntuk nilai. Pola yang sama dipakai standard library padaMap<K, V>.fun <R> jalankan(pemasok: () -> R): R: type parameterRuntuk tipe kembalian. Bound tidak diperlukan karena kita hanya meneruskan nilai daripemasok.class PenampungAngka<T : Number>: bound tunggalT : Number. Setelah bound ini, kita dapat memanggilnilai.toDouble()di dalam methoddoubleNilai.fun <T : Comparable<T>> urutkanDanAmbilMaks: recursive bound.Tharus subtype dariComparable<T>sehingga methodcompareTotersedia. Pemanggilan denganIntdanStringditerima karena keduanya mengimplementasikanComparable.urutkanDanAmbilMaks("Mangga", "Apel", "Pisang"): kompilator menyimpulkanT = String. String dibandingkan secara lexicographic, jadi “Pisang” adalah yang terbesar.
Memilih Konvensi yang Tepat #
Saat menulis class atau function generic, ikuti konvensi yang sudah lazim agar kode konsisten dengan ekosistem. Saat membaca kode Kotlin di standard library atau library populer, kita akan menemui konvensi yang sama. Hal ini mempercepat pemahaman karena pembaca tidak perlu menebak makna setiap huruf.
Referensi Lanjutan #
- Generics
— penjelasan resmi type parameter
T,E,K,Vdi Kotlin - Reified type parameters
— keterhubungan type parameter dengan
reifiedpada inline function
Pada chapter Upper Bounds kita akan membedah lebih jauh bagaimana bound dapat berupa lebih dari satu tipe dengan klausa where, serta implikasinya pada tipe yang boleh dipakai sebagai type argument.