Reflection #
Reflection adalah kemampuan program untuk memeriksa dan memanipulasi strukturnya sendiri di runtime — membaca nama properti, memanggil fungsi berdasarkan namanya, memeriksa tipe tanpa mengetahuinya saat kompilasi. Di Kotlin, reflection hadir melalui kotlin.reflect yang menyediakan API type-safe di atas JVM reflection. Reflection adalah alat yang powerful sekaligus berbahaya: ia membuka pintu untuk hal-hal yang tidak bisa dilakukan dengan cara biasa, tapi dengan biaya performa, keamanan tipe yang berkurang, dan kode yang lebih sulit dipahami. Library-library besar seperti framework serialisasi (Gson, Jackson, kotlinx.serialization), dependency injection (Dagger, Hilt, Koin), dan ORM (Hibernate, Exposed) menggunakan reflection secara internal. Artikel ini membahas seluruh API reflection Kotlin, kapan penggunaannya masuk akal, dan yang terpenting — kapan sebaiknya dihindari.
Setup Reflection #
Di JVM, Kotlin reflection membutuhkan dependency tambahan untuk fungsionalitas penuh.
// build.gradle.kts
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.0")
}
Tanpa dependency ini, beberapa operasi reflection akan melempar KotlinReflectionNotSupportedError. Dependency ini cukup besar (~3MB) — pertimbangkan ini saat membangun aplikasi mobile atau yang sensitif terhadap ukuran.
KClass — Representasi Kelas #
KClass<T> adalah representasi kelas Kotlin di runtime. Ini setara dengan Class<T> di Java tapi dengan API yang lebih kaya dan type-safe.
import kotlin.reflect.KClass
import kotlin.reflect.full.*
data class User(
val id: Long,
val nama: String,
val email: String,
var aktif: Boolean = true
)
// Mendapatkan KClass
val kelas: KClass<User> = User::class
val kelasInstance: KClass<out User> = User("", "", "").javaClass.kotlin
// Informasi dasar
println(kelas.simpleName) // "User"
println(kelas.qualifiedName) // "com.example.User"
println(kelas.isData) // true — data class
println(kelas.isAbstract) // false
println(kelas.isSealed) // false
println(kelas.isFinal) // true (data class bersifat final by default)
println(kelas.visibility) // PUBLIC
// Superclass dan interface
println(kelas.superclasses) // [Any]
println(kelas.supertypes) // [kotlin.Any]
// Companion object
data class Produk(val id: Int, val nama: String) {
companion object {
fun buat(nama: String) = Produk(0, nama)
}
}
val compObject = Produk::class.companionObject
val compInstance = Produk::class.companionObjectInstance
Membuat Instance via Reflection #
// createInstance() — memanggil constructor tanpa argumen
class KonfigurasiDefault {
var host: String = "localhost"
var port: Int = 8080
}
val instance = KonfigurasiDefault::class.createInstance()
println(instance.host) // "localhost"
// Memanggil constructor dengan argumen
val konstruktor = User::class.primaryConstructor
val userBaru = konstruktor?.call(1L, "Andi", "[email protected]", true)
println(userBaru) // User(id=1, nama=Andi, [email protected], aktif=true)
// Mencari constructor berdasarkan parameter
val konstruktorAlternatif = User::class.constructors
.find { it.parameters.size == 3 } // constructor dengan 3 parameter
val userTanpaAktif = konstruktorAlternatif?.call(1L, "Budi", "[email protected]")
// KType — tipe dengan generics
val tipeList: kotlin.reflect.KType = List::class.createType(
listOf(kotlin.reflect.KTypeProjection.invariant(String::class.createType()))
)
KProperty — Properti via Reflection #
KProperty merepresentasikan properti Kotlin dan memungkinkan pembacaan nilai, modifikasi (untuk var), dan pemeriksaan metadata.
import kotlin.reflect.KProperty
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.*
data class Konfigurasi(
var host: String = "localhost",
var port: Int = 8080,
val versi: String = "1.0"
)
val config = Konfigurasi()
// Mendapatkan semua member property
val properties = Konfigurasi::class.memberProperties
properties.forEach { prop ->
println("${prop.name}: ${prop.returnType} = ${prop.get(config)}")
}
// host: kotlin.String = localhost
// port: kotlin.Int = 8080
// versi: kotlin.String = 1.0
// Cek apakah mutable
properties.forEach { prop ->
val isMutable = prop is KMutableProperty<*>
println("${prop.name} mutable: $isMutable")
}
// host mutable: true
// port mutable: true
// versi mutable: false
// Baca nilai properti berdasarkan nama
fun <T : Any> bacaProperti(obj: T, namaProperti: String): Any? {
val prop = obj::class.memberProperties.find { it.name == namaProperti }
return prop?.get(obj)
}
println(bacaProperti(config, "host")) // "localhost"
println(bacaProperti(config, "port")) // 8080
// Tulis nilai properti berdasarkan nama
fun <T : Any> tulisProperti(obj: T, namaProperti: String, nilai: Any?) {
val prop = obj::class.memberProperties
.filterIsInstance<KMutableProperty<*>>()
.find { it.name == namaProperti }
prop?.setter?.call(obj, nilai)
}
tulisProperti(config, "host", "api.example.com")
tulisProperti(config, "port", 443)
println(config) // Konfigurasi(host=api.example.com, port=443, versi=1.0)
Extension Properties via Reflection #
// memberExtensionProperties — extension property yang dideklarasikan di companion/body
class Lingkaran(val radius: Double)
val Lingkaran.luas: Double get() = Math.PI * radius * radius
// Extension properties tidak muncul di memberProperties biasa
// Mereka ada di memberExtensionProperties dari kelas yang mendefinisikannya
KFunction — Fungsi via Reflection #
KFunction merepresentasikan fungsi Kotlin dan memungkinkan pemanggilan fungsi berdasarkan metadata.
import kotlin.reflect.KFunction
import kotlin.reflect.full.*
class KalkuLator {
fun tambah(a: Int, b: Int): Int = a + b
fun kali(a: Double, b: Double): Double = a * b
fun sapa(nama: String = "Dunia"): String = "Halo, $nama!"
private fun rahasiaFunction(): String = "tidak terlihat"
}
val kalk = KalkuLator()
// Mendapatkan semua fungsi member
val fungsi = KalkuLator::class.memberFunctions
fungsi.forEach { fn ->
println("${fn.name}(${fn.parameters.drop(1).joinToString { it.name ?: "?" }}): ${fn.returnType}")
}
// Memanggil fungsi berdasarkan nama
fun panggilFungsi(obj: Any, namaFungsi: String, vararg args: Any?): Any? {
val fn = obj::class.memberFunctions.find { it.name == namaFungsi }
return fn?.call(obj, *args)
}
println(panggilFungsi(kalk, "tambah", 3, 4)) // 7
println(panggilFungsi(kalk, "sapa", "Kotlin")) // "Halo, Kotlin!"
// Fungsi dengan parameter default
val fnSapa = KalkuLator::class.memberFunctions.find { it.name == "sapa" }
// Memanggil dengan parameter default — gunakan callBy
val paramDefault = fnSapa?.parameters?.associateWith { param ->
when (param.kind) {
kotlin.reflect.KParameter.Kind.INSTANCE -> kalk
else -> null // null = gunakan nilai default
}
}?.filterValues { it != null }
val hasilDefault = fnSapa?.callBy(paramDefault ?: emptyMap())
println(hasilDefault) // "Halo, Dunia!" (nama menggunakan default)
// KFunction reference — function reference adalah KFunction
val fnRef: KFunction<Int> = KalkuLator::tambah
println(fnRef.name) // "tambah"
println(fnRef.parameters.size) // 3 (instance + a + b)
println(fnRef.returnType) // kotlin.Int
Annotations via Reflection #
Salah satu penggunaan reflection paling umum adalah membaca annotation di runtime — dasar dari framework serialisasi, validasi, dan ORM.
import kotlin.reflect.full.*
// Definisi annotation
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonField(val nama: String = "")
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Required
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonObject(val nama: String = "")
// Kelas dengan annotation
@JsonObject("user_data")
data class UserDTO(
@JsonField("user_id") val id: Long,
@JsonField("full_name") @Required val nama: String,
@JsonField val email: String,
val password: String // tidak ada JsonField — dikecualikan dari serialisasi
)
// Baca annotation dari kelas
val kelasAnnotation = UserDTO::class.findAnnotation<JsonObject>()
println(kelasAnnotation?.nama) // "user_data"
// Baca annotation dari setiap properti
UserDTO::class.memberProperties.forEach { prop ->
val jsonField = prop.findAnnotation<JsonField>()
val required = prop.findAnnotation<Required>()
if (jsonField != null) {
val namaSerialisasi = jsonField.nama.ifEmpty { prop.name }
println("${prop.name} → '$namaSerialisasi' ${if (required != null) "(required)" else ""}")
} else {
println("${prop.name} → dikecualikan dari serialisasi")
}
}
// id → 'user_id'
// nama → 'full_name' (required)
// email → 'email'
// password → dikecualikan dari serialisasi
Kasus Penggunaan Nyata #
Serialisasi JSON Ringan #
// Serialisasi objek ke Map berdasarkan annotation @JsonField
fun <T : Any> serialisasi(obj: T): Map<String, Any?> {
val kelas = obj::class
val result = mutableMapOf<String, Any?>()
kelas.memberProperties.forEach { prop ->
val jsonField = prop.findAnnotation<JsonField>() ?: return@forEach
val kunci = jsonField.nama.ifEmpty { prop.name }
result[kunci] = prop.get(obj)
}
return result
}
// Validasi berdasarkan @Required
fun <T : Any> validasi(obj: T): List<String> {
val errors = mutableListOf<String>()
obj::class.memberProperties.forEach { prop ->
val required = prop.findAnnotation<Required>() ?: return@forEach
val nilai = prop.get(obj)
if (nilai == null || (nilai is String && nilai.isBlank())) {
errors.add("${prop.name} wajib diisi")
}
}
return errors
}
val user = UserDTO(1L, "Andi", "[email protected]", "secret")
val json = serialisasi(user)
println(json)
// {user_id=1, full_name=Andi, [email protected]}
val errors = validasi(UserDTO(0L, "", "[email protected]", "secret"))
println(errors) // ["nama wajib diisi"]
Dependency Injection Sederhana #
// DI container sederhana berbasis reflection
@Target(AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.RUNTIME)
annotation class Inject
class Container {
private val registry = mutableMapOf<KClass<*>, Any>()
fun <T : Any> daftarkan(kelas: KClass<T>, instance: T) {
registry[kelas] = instance
}
@Suppress("UNCHECKED_CAST")
fun <T : Any> ambil(kelas: KClass<T>): T {
return registry[kelas] as? T ?: buat(kelas)
}
@Suppress("UNCHECKED_CAST")
private fun <T : Any> buat(kelas: KClass<T>): T {
val konstruktor = kelas.constructors
.find { it.findAnnotation<Inject>() != null }
?: kelas.primaryConstructor
?: throw IllegalStateException("Tidak ada constructor yang bisa digunakan")
val argumen = konstruktor.parameters.associateWith { param ->
val tipeParam = param.type.classifier as? KClass<*>
?: throw IllegalStateException("Tidak bisa resolve tipe ${param.type}")
ambil(tipeParam)
}
val instance = konstruktor.callBy(argumen) as T
registry[kelas] = instance
return instance
}
}
// Penggunaan
interface Database { fun query(sql: String): List<Map<String, Any>> }
class DatabaseImpl : Database {
override fun query(sql: String) = listOf(mapOf("id" to 1, "nama" to "Andi"))
}
class UserRepository @Inject constructor(private val db: Database) {
fun semuaUser() = db.query("SELECT * FROM users")
}
class UserService @Inject constructor(private val repo: UserRepository) {
fun daftarUser() = repo.semuaUser()
}
val container = Container()
container.daftarkan(Database::class, DatabaseImpl())
val service = container.ambil(UserService::class)
println(service.daftarUser()) // [{id=1, nama=Andi}]
Copy dengan Modifikasi Dinamis (Mirip data class copy()) #
// Membuat salinan objek dengan properti tertentu diubah
@Suppress("UNCHECKED_CAST")
fun <T : Any> kopiBerubah(obj: T, perubahan: Map<String, Any?>): T {
val kelas = obj::class
val konstruktor = kelas.primaryConstructor
?: throw IllegalStateException("Tidak ada primary constructor")
val argumen = konstruktor.parameters.associateWith { param ->
if (param.name in perubahan) {
perubahan[param.name]
} else {
kelas.memberProperties
.find { it.name == param.name }
?.get(obj)
}
}
return konstruktor.callBy(argumen) as T
}
data class Karyawan(val nama: String, val gaji: Double, val departemen: String)
val karyawan = Karyawan("Andi", 15_000_000.0, "Engineering")
val dinaikan = kopiBerubah(karyawan, mapOf("gaji" to 18_000_000.0))
println(dinaikan) // Karyawan(nama=Andi, gaji=18000000.0, departemen=Engineering)
// Note: untuk kasus sederhana, data class .copy() jauh lebih baik!
// Reflection copy berguna hanya saat nama properti tidak diketahui saat kompilasi
reified Type Parameters #
reified memungkinkan mengakses tipe generik di runtime — biasanya generics dihapus saat kompilasi (type erasure), tapi reified di fungsi inline menjaga informasi tipe.
// Tanpa reified — tidak bisa akses T di runtime
fun <T> ambilInstans(list: List<Any>): List<T> {
// return list.filterIsInstance<T>() // ERROR: tidak bisa karena type erasure
return list.filterIsInstance<Any>() as List<T> // tidak aman
}
// Dengan reified inline function — T tersedia di runtime
inline fun <reified T> ambilInstans(list: List<Any>): List<T> {
return list.filterIsInstance<T>() // OK karena reified
}
val campuran: List<Any> = listOf(1, "dua", 3.0, "empat", 5, true)
val hanyaString = ambilInstans<String>(campuran) // ["dua", "empat"]
val hanyaInt = ambilInstans<Int>(campuran) // [1, 5]
// Akses KClass dari reified type
inline fun <reified T : Any> namaKelas(): String = T::class.simpleName ?: "Unknown"
println(namaKelas<User>()) // "User"
println(namaKelas<List<String>>()) // "List"
// Factory function dengan reified
inline fun <reified T : Any> buatDariMap(data: Map<String, Any?>): T {
val konstruktor = T::class.primaryConstructor
?: throw IllegalStateException("Tidak ada primary constructor untuk ${T::class.simpleName}")
val argumen = konstruktor.parameters.associateWith { param ->
data[param.name]
}
return konstruktor.callBy(argumen)
}
val userData = mapOf("id" to 1L, "nama" to "Andi", "email" to "[email protected]", "aktif" to true)
val user = buatDariMap<User>(userData)
println(user) // User(id=1, nama=Andi, [email protected], aktif=true)
// Gson-style type token dengan reified
inline fun <reified T> Gson.fromJsonTyped(json: String): T =
fromJson(json, T::class.java)
// val users: List<User> = gson.fromJsonTyped("[{...}]") // dengan reified
Performa dan Kapan Harus Menghindari Reflection #
Reflection bukan gratis — ada overhead yang signifikan dibanding akses langsung.
import kotlin.time.measureTime
data class Titik(val x: Double, val y: Double)
val titik = Titik(3.0, 4.0)
// Akses langsung — nanoseconds
val waktuLangsung = measureTime {
repeat(1_000_000) { titik.x + titik.y }
}
// Akses via reflection — microseconds (ratusan kali lebih lambat)
val propX = Titik::class.memberProperties.find { it.name == "x" }!!
val propY = Titik::class.memberProperties.find { it.name == "y" }!!
val waktuRefleksi = measureTime {
repeat(1_000_000) { propX.get(titik) as Double + propY.get(titik) as Double }
}
println("Langsung: $waktuLangsung")
println("Reflection: $waktuRefleksi")
// Reflection bisa 10-100x lebih lambat tergantung operasi
Optimasi: Cache KProperty dan KFunction #
// ANTI-PATTERN: resolusi ulang setiap pemanggilan
fun bacaNilai(obj: Any, namaProperti: String): Any? {
return obj::class.memberProperties // alokasi baru setiap kali!
.find { it.name == namaProperti }
?.get(obj)
}
// BENAR: cache hasil resolusi
object PropertyCache {
private val cache = mutableMapOf<Pair<KClass<*>, String>, kotlin.reflect.KProperty1<*, *>>()
@Suppress("UNCHECKED_CAST")
fun <T : Any> ambil(kelas: KClass<T>, nama: String): kotlin.reflect.KProperty1<T, *>? {
val kunci = kelas to nama
return cache.getOrPut(kunci) {
kelas.memberProperties.find { it.name == nama } ?: return null
} as kotlin.reflect.KProperty1<T, *>?
}
}
fun <T : Any> bacaNilaiCached(obj: T, namaProperti: String): Any? =
PropertyCache.ambil(obj::class, namaProperti)?.get(obj)
Kapan Menggunakan vs Menghindari #
flowchart TD
A{Apakah tipe diketahui\nsaat kompilasi?} -- Ya --> B["Gunakan akses langsung\nobj.property, obj.method()"]
A -- Tidak --> C{Apakah ini\nframework/library?}
C -- Ya --> D["Reflection bisa diterima\nSerialisasi, DI, ORM\nCache KProperty/KFunction"]
C -- Tidak --> E{Alternatif lain?}
E -- Ada --> F["Gunakan interface\nsealed class\natau generics"]
E -- Tidak Ada --> G["Reflection dengan hati-hati\nDocumentasikan alasannya\nCache resolusi\nTest performa"]Gunakan Reflection jika:
✓ Membangun framework atau library generik
✓ Serialisasi/deserialisasi tipe yang tidak diketahui saat kompilasi
✓ Dependency injection container
✓ ORM mapping antara objek dan tabel database
✓ Plugin system yang load kode dinamis
✓ Tool debugging dan introspeksi
Hindari Reflection jika:
✗ Tipe sudah diketahui saat kompilasi — gunakan akses langsung
✗ Di hot path atau loop yang dieksekusi jutaan kali
✗ Kode sudah bisa dicapai dengan interface atau generics
✗ Aplikasi mobile/embedded yang sensitif ukuran
✗ Ketika keamanan tipe di compile-time lebih penting dari fleksibilitas runtime
Sealed Class dan when via Reflection #
// Mendapatkan semua subclass sealed class
sealed class Bentuk {
data class Lingkaran(val radius: Double) : Bentuk()
data class Persegi(val sisi: Double) : Bentuk()
data class Segitiga(val alas: Double, val tinggi: Double) : Bentuk()
}
val semuaBentuk = Bentuk::class.sealedSubclasses
semuaBentuk.forEach { subkelas ->
println("Subclass: ${subkelas.simpleName}")
}
// Subclass: Lingkaran
// Subclass: Persegi
// Subclass: Segitiga
// Berguna untuk tools: auto-generate dokumentasi, test cases, UI forms
fun buatFormDariSealedClass(sealed: KClass<*>): List<String> {
return sealed.sealedSubclasses.map { subkelas ->
val params = subkelas.primaryConstructor?.parameters
?.joinToString(", ") { "${it.name}: ${it.type}" }
?: ""
"${subkelas.simpleName}($params)"
}
}
println(buatFormDariSealedClass(Bentuk::class))
// ["Lingkaran(radius: kotlin.Double)", "Persegi(sisi: kotlin.Double)", ...]
Ringkasan #
KClass<T>adalah representasi kelas Kotlin di runtime — dapatkan denganNamaKelas::classatauobjek::class. Menyediakan informasi sepertisimpleName,memberProperties,memberFunctions,primaryConstructor.KPropertydanKMutablePropertymerepresentasikan properti — gunakan.get(instance)untuk membaca dan.setter.call(instance, nilai)untuk menulis. Selalu periksa apakah propertiis KMutableProperty<*>sebelum mencoba menulis.KFunctionmerepresentasikan fungsi — gunakan.call(instance, *args)untuk memanggil atau.callBy(mapOf(param to nilai))untuk mendukung parameter default.findAnnotation<T>()untuk membaca annotation dari property, fungsi, atau kelas di runtime — fondasi dari framework serialisasi dan validasi.reifiedtype parameters diinlinefunction mengatasi type erasure — memungkinkan aksesT::classdi dalam fungsi generik. Gunakan ini daripada menerimaKClass<T>sebagai parameter eksplisit.- Cache
KPropertydanKFunctionyang sudah diresolusi — resolusi ulang setiap pemanggilan sangat mahal. GunakanMapatauConcurrentHashMapsebagai cache untuk hot path.- Reflection 10-100x lebih lambat dari akses langsung. Jangan gunakan di inner loop atau kode yang dieksekusi sangat sering. Profil dulu sebelum mengoptimalkan.
sealedSubclassesuntuk mendapatkan semua subclass sealed class secara programatik — berguna untuk tools, dokumentasi otomatis, atau test case generation.- Prefer interface dan generics sebelum reflection — jika kamu tahu tipenya saat kompilasi, selalu gunakan akses langsung. Reflection hanya untuk kasus di mana tipe benar-benar tidak diketahui saat kompilasi.
- Reflection paling tepat digunakan di dalam library dan framework, bukan di kode aplikasi biasa. Jika kamu menggunakan reflection di business logic, tanyakan pada diri sendiri: apakah ada cara yang lebih type-safe untuk mencapai hal yang sama?