Sering kita ingin membuat tipe baru untuk membedakan nilai yang sebenarnya memiliki tipe primitif yang sama — misalnya Email, IdPengguna, dan KodePos semuanya pada dasarnya adalah String atau Long. Membungkus primitif ke dalam class biasa memberikan keamanan tipe namun menambah overhead alokasi karena setiap nilai menjadi object baru di heap. Kotlin menyediakan @JvmInline value class — class yang dibungkus pada saat kompilasi menjadi tipe primitif yang mendasarinya, sehingga tidak ada alokasi object tambahan. Pada chapter ini kita akan mendeklarasikan value class, menambahkan method, mengimplementasikan interface, dan memvalidasi lewat init block.

Apa Itu Value Class #

Value class adalah class yang dideklarasikan dengan @JvmInline value class. Anotasi @JvmInline menandakan bahwa kompilator harus menginline property value class ke tipe yang mendasarinya — biasanya String, Int, Long, atau tipe primitif lain. Karena diinline, instance value class tidak menjadi object baru di heap; kode yang memakai value class diubah menjadi kode yang langsung memakai primitif tersebut. Hasilnya: keamanan tipe pada saat kompilasi tanpa overhead alokasi pada saat runtime.

Syarat utama value class:

  • Harus memiliki persis satu property read-only (val) di primary constructor.
  • Tidak boleh memiliki var di primary constructor.
  • Tidak boleh di-open, abstract, atau sealed.
  • Dapat mengimplementasikan interface (tidak boleh mewarisi class).
  • Dapat memiliki init block, method, dan computed property.

Deklarasi Value Class #

Contoh berikut mendefinisikan empat value class — Email, IdPengguna, SuhuCelsius, dan KodePos. Email membungkus String, IdPengguna membungkus Long, SuhuCelsius membungkus Double dan mengimplementasikan Comparable, dan KodePos memvalidasi format di init block.

kotlin
// value_classes.kt
// Mendemonstrasikan `@JvmInline value class` — wrapper tanpa overhead alokasi.

@JvmInline
value class Email(val value: String) {
    fun domain(): String = value.substringAfter('@', "(tidak valid)")
    fun isValid(): Boolean = value.contains('@') && value.contains('.')
}

@JvmInline
value class IdPengguna(val raw: Long) {
    fun sebagaiString(): String = "USR-${raw.toString().padStart(6, '0')}"
}

@JvmInline
value class SuhuCelsius(val derajat: Double) : Comparable<SuhuCelsius> {
    override fun compareTo(other: SuhuCelsius): Int = derajat.compareTo(other.derajat)
    fun keFahrenheit(): Double = derajat * 9 / 5 + 32
}

// Value class dengan validasi init block.
@JvmInline
value class KodePos(val nilai: String) {
    init {
        require(nilai.length == 5 && nilai.all { it.isDigit() }) {
            "KodePos harus 5 digit angka"
        }
    }
}

data class Pelanggan(val id: IdPengguna, val nama: String, val email: Email)

fun main() {
    val email1 = Email("[email protected]")
    val email2 = Email("[email protected]")
    val email3 = Email("budi-tanpa-at")

    println("Email1: ${email1.value}, domain: ${email1.domain()}, valid: ${email1.isValid()}")
    println("Email2: ${email2.value}, domain: ${email2.domain()}, valid: ${email2.isValid()}")
    println("Email3: ${email3.value}, domain: ${email3.domain()}, valid: ${email3.isValid()}")
    println()

    val id1 = IdPengguna(42L)
    val id2 = IdPengguna(7L)
    println("Id1: ${id1.sebagaiString()}, raw: ${id1.raw}")
    println("Id2: ${id2.sebagaiString()}, raw: ${id2.raw}")
    println()

    val t1 = SuhuCelsius(25.0)
    val t2 = SuhuCelsius(30.0)
    println("Suhu1 = ${t1.derajat} C = ${t1.keFahrenheit()} F")
    println("Suhu2 = ${t2.derajat} C = ${t2.keFahrenheit()} F")
    println("t1 < t2 ? ${t1 < t2}")
    println("t2 > t1 ? ${t2 > t1}")
    println()

    val kodePos1 = KodePos("12345")
    val kodePos2 = KodePos("67890")
    println("KodePos1 = ${kodePos1.nilai}")
    println("KodePos2 = ${kodePos2.nilai}")
    println()

    val pelanggan = Pelanggan(IdPengguna(123L), "Budi", Email("[email protected]"))
    println("Pelanggan: ${pelanggan.id.sebagaiString()} - ${pelanggan.nama} <${pelanggan.email.value}>")
    println("Domain email pelanggan: ${pelanggan.email.domain()}")
}

Output yang Dihasilkan:

text
Email1: [email protected], domain: example.com, valid: true
Email2: [email protected], domain: example.com, valid: true
Email3: budi-tanpa-at, domain: (tidak valid), valid: false

Id1: USR-000042, raw: 42
Id2: USR-000007, raw: 7

Suhu1 = 25.0 C = 77.0 F
Suhu2 = 30.0 C = 86.0 F
t1 < t2 ? true
t2 > t1 ? true

KodePos1 = 12345
KodePos2 = 67890

Pelanggan: USR-000123 - Budi <[email protected]>
Domain email pelanggan: example.com

Mari kita bedah kode di atas:

  1. @JvmInline value class Email(val value: String): anotasi @JvmInline wajib untuk value class di Kotlin/JVM. Property tunggal value: String adalah property utama yang akan diinline. Setelah kompilasi, instance Email("[email protected]") tidak menjadi object baru — kompilator mengganti Email.value menjadi String biasa di seluruh kode.
  2. fun domain(): String = value.substringAfter('@', "(tidak valid)"): value class dapat memiliki method. Method domain mengakses property value dan mengembalikan bagian setelah tanda @. Pemanggilan email1.domain() mengembalikan "example.com" — string domain dari email.
  3. fun isValid(): Boolean = value.contains('@') && value.contains('.'): method validasi sederhana. Pemanggilan mengembalikan true untuk email valid dan false untuk email tanpa @.
  4. @JvmInline value class IdPengguna(val raw: Long): value class untuk ID berbasis Long. Method sebagaiString() memformat raw menjadi string dengan padding nol — 42L menjadi "USR-000042". Karena diinline, pemakaian IdPengguna tidak menambah alokasi object — Long langsung dipakai.
  5. @JvmInline value class SuhuCelsius(val derajat: Double) : Comparable<SuhuCelsius>: value class dapat mengimplementasikan interface. Comparable<SuhuCelsius> memberi method compareTo, sehingga operator < dan > dapat dipakai pada instance SuhuCelsius. Output t1 < t2 ? true membuktikan perbandingan bekerja — 25.0 < 30.0.
  6. init { require(nilai.length == 5 && nilai.all { it.isDigit() }) { ... } }: value class dapat memiliki init block yang memvalidasi nilai saat instance dibuat. require melempar IllegalArgumentException bila kondisi tidak terpenuhi. KodePos("12345") valid, namun KodePos("abcde") akan melempar exception.
  7. data class Pelanggan(val id: IdPengguna, val nama: String, val email: Email): value class dapat dipakai sebagai property di data class biasa. Pelanggan memiliki property bertipe IdPengguna dan Email — bukan Long dan String mentah. Pemanggilan pelanggan.email.domain() menunjukkan bahwa method value class tetap dapat dipanggil dari property.
  8. Output Email3: budi-tanpa-at tidak memiliki @, sehingga domain() mengembalikan "(tidak valid)" dan isValid() mengembalikan false. Validasi ini menjaga invariant tipe — instance Email yang tidak valid tetap dapat dibuat, namun pemanggil method dapat memeriksa dengan isValid().

Inlining pada Saat Kompilasi #

Karena value class diinline, bytecode yang dihasilkan tidak memiliki class object tambahan untuk instance Email, IdPengguna, dan SuhuCelsius. Method pada value class diubah menjadi method statis yang menerima primitif sebagai parameter. Hal ini membuat value class mirip dengan primitif yang diberi method — efisien seperti primitif namun memiliki API seperti class.

Inlining tidak selalu sempurna. Ketika instance value class dimasukkan ke koleksi generik (misalnya List<Email>) atau dipakai sebagai argumen variadic Any, kompilator tetap melakukan boxing — membungkus primitif ke dalam instance value class. Boxing hanya terjadi pada posisi yang memang membutuhkan object, sehingga overhead tetap minimal dibanding class biasa.

Kapan Memakai Value Class #

Value class cocok ketika kita ingin:

  • Membedakan tipe primitif yang serupa — Email, IdPengguna, KodePos, Url, TokenOtentikasi. Semua berbasis String namun secara semantik berbeda.
  • Menambahkan method ke tipe primitif — SuhuCelsius punya method keFahrenheit(), IdPengguna punya sebagaiString().
  • Memvalidasi invariant saat pembuatan instance — KodePos menolak string yang bukan 5 digit.
  • Menghindari kesalahan tipe — function kirim(email: Email) tidak akan menerima String biasa atau IdPengguna yang juga berbasis String.

Referensi Lanjutan #

  • Inline classes — panduan resmi value class (sebelumnya inline class)

Ringkasan #

@JvmInline value class membungkus primitif dengan keamanan tipe pada saat kompilasi tanpa overhead alokasi pada saat runtime. Value class wajib memiliki satu property val, dapat memiliki method, init block, dan mengimplementasikan interface. Value class cocok untuk merepresentasikan tipe domain spesifik berbasis primitif — Email, IdPengguna, SuhuCelsius, KodePos. Pada chapter Delegation berikutnya, kita akan melihat pola desain delegation yang lebih luas dari interface delegation sebelumnya.