Generics memungkinkan satu definisi class bekerja untuk berbagai tipe data tanpa kita menulis ulang kode yang sama. Bayangkan kita ingin membuat class wadah yang bisa menyimpan Int, String, atau Pengguna — tanpa generics, kita harus menulis tiga class berbeda atau memakai Any dan kehilangan type safety pada saat kompilasi. Dengan generics, kita menulis satu definisi class Box<T> dan kompilator tetap mengetahui tipe setiap instance. Pada chapter ini kita akan membedah cara mendeklarasikan class generic, menginstantiasinya dengan tipe konkret, serta memakai lebih dari satu type parameter dalam satu class.

Apa Itu Type Parameter #

Type parameter adalah placeholder tipe yang ditulis di dalam tanda kurung siku setelah nama class. Kompilator akan mengganti placeholder tersebut dengan tipe konkret saat kita menginstantiasi class. Konvensi penamaan type parameter memakai huruf kapital tunggal seperti T untuk Type, E untuk Element, atau K dan V untuk Key dan Value.

Bentuk paling sederhana adalah class Box<T>(val value: T). Tanda T di sini berfungsi sebagai placeholder yang akan diisi saat deklarasi variabel. Kompilator memverifikasi setiap akses value agar sesuai dengan tipe yang kita berikan, sehingga kode yang mencampur Box<Int> dengan Box<String> akan ditolak sebelum program dijalankan.

kotlin Deklarasi generic class dasar
class Box<T>(val value: T)

Membuat dan Menggunakan Box #

Sekarang mari kita lihat class Box lengkap dengan beberapa method, lalu instantiasi dengan tipe yang berbeda:

kotlin
// Chapter: Generic Classes
// Mendefinisikan class generic dengan satu dan beberapa type parameter.

class Box<T>(val value: T) {
    fun deskripsi(): String = "Box berisi nilai $value bertipe ${value!!::class.simpleName}"
}

class Pair<A, B>(val first: A, val second: B) {
    fun tukar(): Pair<B, A> = Pair(second, first)
}

class Tumpukan<T> {
    private val penyimpanan = mutableListOf<T>()

    fun dorong(item: T) {
        penyimpanan.add(item)
    }

    fun ambil(): T? = if (penyimpanan.isEmpty()) null else penyimpanan.removeAt(penyimpanan.lastIndex)

    fun ukuran(): Int = penyimpanan.size
}

fun main() {
    val kotakInt = Box<Int>(5)
    println(kotakInt.value)
    println(kotakInt.deskripsi())

    val kotakString = Box("Halo Kotlin")
    println(kotakString.value)
    println(kotakString.deskripsi())

    val pasangan = Pair<String, Int>("Budi", 25)
    println("First: ${pasangan.first}, Second: ${pasangan.second}")
    val ditukar = pasangan.tukar()
    println("Setelah ditukar -> First: ${ditukar.first}, Second: ${ditukar.second}")

    val tumpukanInt = Tumpukan<Int>()
    tumpukanInt.dorong(10)
    tumpukanInt.dorong(20)
    tumpukanInt.dorong(30)
    println("Ukuran tumpukan: ${tumpukanInt.ukuran()}")
    println("Ambil: ${tumpukanInt.ambil()}")
    println("Ambil: ${tumpukanInt.ambil()}")
    println("Ambil: ${tumpukanInt.ambil()}")
    println("Ambil dari tumpukan kosong: ${tumpukanInt.ambil()}")
}

Output yang Dihasilkan:

text
5
Box berisi nilai 5 bertipe Int
Halo Kotlin
Box berisi nilai Halo Kotlin bertipe String
First: Budi, Second: 25
Setelah ditukar -> First: 25, Second: Budi
Ukuran tumpukan: 3
Ambil: 30
Ambil: 20
Ambil: 10
Ambil dari tumpukan kosong: null

Mari kita telaah baris-baris kode di atas:

  1. class Box<T>(val value: T): T adalah type parameter. Konstruktor menerima satu argumen value bertipe T. Saat kita menulis Box<Int>(5), kompilator mengikat T ke Int, sehingga value bertipe Int.
  2. fun deskripsi(): method ini memakai properti value yang bertipe T. Ekspresi value!!::class.simpleName mengambil nama kelas runtime dari nilai, sehingga pesan yang dicetak informatif untuk setiap tipe.
  3. class Pair<A, B>(val first: A, val second: B): class generic dengan dua type parameter. Hal ini berguna ketika kita ingin membungpas dua nilai berbeda tipe seperti String dan Int.
  4. fun tukar(): Pair<B, A>: method yang mengembalikan instance Pair baru dengan tipe parameter terbalik. Perhatikan bahwa tipe kembalian adalah Pair<B, A>, bukan Pair<A, B>.
  5. class Tumpukan<T>: class generic tanpa argumen konstruktor. Type parameter T dipakai di method dorong(item: T) dan ambil(): T? sehingga kita bisa membuat tumpukan untuk tipe apa pun.
  6. val tumpukanInt = Tumpukan<Int>(): kita menentukan tipe eksplisit Int saat instantiasi. Kompilator memverifikasi bahwa hanya Int yang boleh dimasukkan ke dalam tumpukan ini.
  7. Box("Halo Kotlin"): tanpa menulis Box<String>, kompilator menyimpulkan type parameter dari argumen yang diberikan. Hal ini disebut type inference dan membuat kode lebih ringkas.

Beberapa Type Parameter Sekaligus #

Class generic tidak terbatas pada satu type parameter. Pair<A, B> di atas adalah contoh sederhana. Pola yang sama dipakai oleh Map<K, V> di standard library Kotlin, di mana K untuk tipe kunci dan V untuk tipe nilai. Kita juga bisa membuat class dengan tiga atau lebih type parameter, namun semakin banyak type parameter, semakin sulit kode dibaca.

Mengapa Generic Class Penting #

Tanpa generics, kita akan terpaksa memakai Any dan melakukan cast manual setiap kali mengakses nilai. Pendekatan tersebut rentan galat runtime karena cast bisa gagal. Dengan generic class, kompilator melakukan verifikasi tipe saat kompilasi sehingga banyak galat tertangkap lebih awal. Hal ini menjadi fondasi penting sebelum kita melangkah ke generic functions yang akan kita bahas pada chapter berikutnya.

Referensi Lanjutan #

  • Generics — panduan resmi deklarasi generic class dengan type parameter T
  • Any API reference — API reference Any sebagai supertype yang dipakai bila generics tidak tersedia

Pada chapter Generic Functions kita akan melihat bagaimana ide yang sama dapat diterapkan pada level function, sehingga satu definisi function dapat menerima dan mengembalikan tipe apa pun dengan tetap menjaga type safety.