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
vardi primary constructor. - Tidak boleh di-
open,abstract, atausealed. - Dapat mengimplementasikan interface (tidak boleh mewarisi class).
- Dapat memiliki
initblock, 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.
// 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:
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.comMari kita bedah kode di atas:
@JvmInline value class Email(val value: String): anotasi@JvmInlinewajib untuk value class di Kotlin/JVM. Property tunggalvalue: Stringadalah property utama yang akan diinline. Setelah kompilasi, instanceEmail("[email protected]")tidak menjadi object baru — kompilator menggantiEmail.valuemenjadiStringbiasa di seluruh kode.fun domain(): String = value.substringAfter('@', "(tidak valid)"): value class dapat memiliki method. Methoddomainmengakses propertyvaluedan mengembalikan bagian setelah tanda@. Pemanggilanemail1.domain()mengembalikan"example.com"— string domain dari email.fun isValid(): Boolean = value.contains('@') && value.contains('.'): method validasi sederhana. Pemanggilan mengembalikantrueuntuk email valid danfalseuntuk email tanpa@.@JvmInline value class IdPengguna(val raw: Long): value class untuk ID berbasisLong. MethodsebagaiString()memformatrawmenjadi string dengan padding nol —42Lmenjadi"USR-000042". Karena diinline, pemakaianIdPenggunatidak menambah alokasi object —Longlangsung dipakai.@JvmInline value class SuhuCelsius(val derajat: Double) : Comparable<SuhuCelsius>: value class dapat mengimplementasikan interface.Comparable<SuhuCelsius>memberi methodcompareTo, sehingga operator<dan>dapat dipakai pada instanceSuhuCelsius. Outputt1 < t2 ? truemembuktikan perbandingan bekerja —25.0 < 30.0.init { require(nilai.length == 5 && nilai.all { it.isDigit() }) { ... } }: value class dapat memilikiinitblock yang memvalidasi nilai saat instance dibuat.requiremelemparIllegalArgumentExceptionbila kondisi tidak terpenuhi.KodePos("12345")valid, namunKodePos("abcde")akan melempar exception.data class Pelanggan(val id: IdPengguna, val nama: String, val email: Email): value class dapat dipakai sebagai property di data class biasa.Pelangganmemiliki property bertipeIdPenggunadanEmail— bukanLongdanStringmentah. Pemanggilanpelanggan.email.domain()menunjukkan bahwa method value class tetap dapat dipanggil dari property.- Output
Email3:budi-tanpa-attidak memiliki@, sehinggadomain()mengembalikan"(tidak valid)"danisValid()mengembalikanfalse. Validasi ini menjaga invariant tipe — instanceEmailyang tidak valid tetap dapat dibuat, namun pemanggil method dapat memeriksa denganisValid().
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 berbasisStringnamun secara semantik berbeda. - Menambahkan method ke tipe primitif —
SuhuCelsiuspunya methodkeFahrenheit(),IdPenggunapunyasebagaiString(). - Memvalidasi invariant saat pembuatan instance —
KodePosmenolak string yang bukan 5 digit. - Menghindari kesalahan tipe — function
kirim(email: Email)tidak akan menerimaStringbiasa atauIdPenggunayang juga berbasisString.
Referensi Lanjutan #
- Inline classes
— panduan resmi
value class(sebelumnyainline 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.