Reflection #
Reflection is a program’s ability to inspect and manipulate its own structure at runtime — reading property names, calling functions by name, examining types without knowing them at compile time. In Kotlin, reflection comes through kotlin.reflect, which provides a type-safe API on top of JVM reflection. Reflection is a powerful and dangerous tool at the same time: it opens doors to things that can’t be done the ordinary way, but with a performance cost, reduced type safety, and code that’s harder to understand. Major libraries like serialization frameworks (Gson, Jackson, kotlinx.serialization), dependency injection (Dagger, Hilt, Koin), and ORMs (Hibernate, Exposed) use reflection internally. This article covers the entire Kotlin reflection API, when its use makes sense, and most importantly — when it should be avoided.
Setting Up Reflection #
On the JVM, Kotlin reflection requires an additional dependency for full functionality.
// build.gradle.kts
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.0")
}
Without this dependency, some reflection operations will throw KotlinReflectionNotSupportedError. This dependency is fairly large (~3MB) — consider this when building mobile apps or size-sensitive applications.
KClass — Class Representation #
KClass<T> is the runtime representation of a Kotlin class. It’s equivalent to Java’s Class<T> but with a richer, type-safe API.
import kotlin.reflect.KClass
import kotlin.reflect.full.*
data class User(
val id: Long,
val nama: String,
val email: String,
var aktif: Boolean = true
)
// Getting a KClass
val kelas: KClass<User> = User::class
val kelasInstance: KClass<out User> = User("", "", "").javaClass.kotlin
// Basic information
println(kelas.simpleName) // "User"
println(kelas.qualifiedName) // "com.example.User"
println(kelas.isData) // true — a data class
println(kelas.isAbstract) // false
println(kelas.isSealed) // false
println(kelas.isFinal) // true (data classes are final by default)
println(kelas.visibility) // PUBLIC
// Superclasses and interfaces
println(kelas.superclasses) // [Any]
println(kelas.supertypes) // [kotlin.Any]
// Companion objects
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
Creating Instances via Reflection #
// createInstance() — calls the no-argument constructor
class KonfigurasiDefault {
var host: String = "localhost"
var port: Int = 8080
}
val instance = KonfigurasiDefault::class.createInstance()
println(instance.host) // "localhost"
// Calling a constructor with arguments
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)
// Finding a constructor by its parameters
val konstruktorAlternatif = User::class.constructors
.find { it.parameters.size == 3 } // a constructor with 3 parameters
val userTanpaAktif = konstruktorAlternatif?.call(1L, "Budi", "[email protected]")
// KType — types with generics
val tipeList: kotlin.reflect.KType = List::class.createType(
listOf(kotlin.reflect.KTypeProjection.invariant(String::class.createType()))
)
KProperty — Properties via Reflection #
KProperty represents a Kotlin property and enables reading values, modifying (for vars), and inspecting 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()
// Getting all member properties
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
// Checking mutability
properties.forEach { prop ->
val isMutable = prop is KMutableProperty<*>
println("${prop.name} mutable: $isMutable")
}
// host mutable: true
// port mutable: true
// versi mutable: false
// Reading a property value by name
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
// Writing a property value by name
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 properties declared in the companion/body
class Lingkaran(val radius: Double)
val Lingkaran.luas: Double get() = Math.PI * radius * radius
// Extension properties don't appear in the regular memberProperties
// They live in memberExtensionProperties of the class that defines them
KFunction — Functions via Reflection #
KFunction represents a Kotlin function and enables calling functions based on 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 = "Hello, $nama!"
private fun rahasiaFunction(): String = "not visible"
}
val kalk = Kalkulator()
// Getting all member functions
val fungsi = Kalkulator::class.memberFunctions
fungsi.forEach { fn ->
println("${fn.name}(${fn.parameters.drop(1).joinToString { it.name ?: "?" }}): ${fn.returnType}")
}
// Calling a function by name
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")) // "Hello, Kotlin!"
// Functions with default parameters
val fnSapa = Kalkulator::class.memberFunctions.find { it.name == "sapa" }
// Calling with default parameters — use callBy
val paramDefault = fnSapa?.parameters?.associateWith { param ->
when (param.kind) {
kotlin.reflect.KParameter.Kind.INSTANCE -> kalk
else -> null // null = use the default value
}
}?.filterValues { it != null }
val hasilDefault = fnSapa?.callBy(paramDefault ?: emptyMap())
println(hasilDefault) // "Hello, Dunia!" (nama uses its default)
// KFunction references — a function reference is a 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 #
One of the most common uses of reflection is reading annotations at runtime — the foundation of serialization, validation, and ORM frameworks.
import kotlin.reflect.full.*
// Defining annotations
@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 = "")
// A class with annotations
@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 // no JsonField — excluded from serialization
)
// Reading annotations from the class
val kelasAnnotation = UserDTO::class.findAnnotation<JsonObject>()
println(kelasAnnotation?.nama) // "user_data"
// Reading annotations from every property
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} → excluded from serialization")
}
}
// id → 'user_id'
// nama → 'full_name' (required)
// email → 'email'
// password → excluded from serialization
Real Use Cases #
Lightweight JSON Serialization #
// Serializing an object to a Map based on the @JsonField annotation
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
}
// Validation based on @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} is required")
}
}
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 is required"]
Simple Dependency Injection #
// A simple reflection-based DI container
@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("No usable constructor")
val argumen = konstruktor.parameters.associateWith { param ->
val tipeParam = param.type.classifier as? KClass<*>
?: throw IllegalStateException("Can't resolve type ${param.type}")
ambil(tipeParam)
}
val instance = konstruktor.callBy(argumen) as T
registry[kelas] = instance
return instance
}
}
// Usage
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}]
Copying with Dynamic Modifications (Like data class copy()) #
// Creating an object copy with certain properties changed
@Suppress("UNCHECKED_CAST")
fun <T : Any> kopiBerubah(obj: T, perubahan: Map<String, Any?>): T {
val kelas = obj::class
val konstruktor = kelas.primaryConstructor
?: throw IllegalStateException("No 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: for simple cases, the data class .copy() is far better!
// Reflection copy is only useful when property names aren't known at compile time
reified Type Parameters #
reified makes it possible to access generic types at runtime — generics are normally erased at compile time (type erasure), but reified in inline functions preserves the type information.
// Without reified — can't access T at runtime
fun <T> ambilInstans(list: List<Any>): List<T> {
// return list.filterIsInstance<T>() // ERROR: impossible due to type erasure
return list.filterIsInstance<Any>() as List<T> // not safe
}
// With a reified inline function — T is available at runtime
inline fun <reified T> ambilInstans(list: List<Any>): List<T> {
return list.filterIsInstance<T>() // OK because of 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]
// Accessing KClass from a reified type
inline fun <reified T : Any> namaKelas(): String = T::class.simpleName ?: "Unknown"
println(namaKelas<User>()) // "User"
println(namaKelas<List<String>>()) // "List"
// A factory function with reified
inline fun <reified T : Any> buatDariMap(data: Map<String, Any?>): T {
val konstruktor = T::class.primaryConstructor
?: throw IllegalStateException("No primary constructor for ${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)
// A Gson-style type token with reified
inline fun <reified T> Gson.fromJsonTyped(json: String): T =
fromJson(json, T::class.java)
// val users: List<User> = gson.fromJsonTyped("[{...}]") // with reified
Performance and When to Avoid Reflection #
Reflection isn’t free — there’s significant overhead compared to direct access.
import kotlin.time.measureTime
data class Titik(val x: Double, val y: Double)
val titik = Titik(3.0, 4.0)
// Direct access — nanoseconds
val waktuLangsung = measureTime {
repeat(1_000_000) { titik.x + titik.y }
}
// Access via reflection — microseconds (hundreds of times slower)
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("Direct: $waktuLangsung")
println("Reflection: $waktuRefleksi")
// Reflection can be 10-100x slower depending on the operation
Optimization: Cache KProperty and KFunction #
// ANTI-PATTERN: re-resolving on every call
fun bacaNilai(obj: Any, namaProperti: String): Any? {
return obj::class.memberProperties // a new allocation every time!
.find { it.name == namaProperti }
?.get(obj)
}
// CORRECT: cache the resolution results
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)
When to Use vs Avoid #
flowchart TD
A{Is the type known\nat compile time?} -- Yes --> B["Use direct access\nobj.property, obj.method()"]
A -- No --> C{Is this a\nframework/library?}
C -- Yes --> D["Reflection is acceptable\nSerialization, DI, ORM\nCache KProperty/KFunction"]
C -- No --> E{Any alternatives?}
E -- Yes --> F["Use interfaces\nsealed classes\nor generics"]
E -- No --> G["Reflection with care\nDocument the reason\nCache resolutions\nTest performance"]Use Reflection if:
✓ Building a framework or generic library
✓ Serializing/deserializing types unknown at compile time
✓ A dependency injection container
✓ ORM mapping between objects and database tables
✓ A plugin system that loads dynamic code
✓ Debugging and introspection tools
Avoid Reflection if:
✗ The type is already known at compile time — use direct access
✗ In hot paths or loops executed millions of times
✗ The code can already be achieved with interfaces or generics
✗ Size-sensitive mobile/embedded applications
✗ When compile-time type safety matters more than runtime flexibility
Sealed Classes and when via Reflection #
// Getting all subclasses of a 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
// Useful for tools: auto-generating documentation, 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)", ...]
Summary #
KClass<T>is the runtime representation of a Kotlin class — get it withNamaKelas::classorobjek::class. Provides information likesimpleName,memberProperties,memberFunctions,primaryConstructor.KPropertyandKMutablePropertyrepresent properties — use.get(instance)to read and.setter.call(instance, nilai)to write. Always check whether the propertyis KMutableProperty<*>before trying to write.KFunctionrepresents a function — use.call(instance, *args)to invoke or.callBy(mapOf(param to nilai))to support default parameters.findAnnotation<T>()to read annotations from properties, functions, or classes at runtime — the foundation of serialization and validation frameworks.reifiedtype parameters ininlinefunctions overcome type erasure — enablingT::classaccess inside generic functions. Use this instead of acceptingKClass<T>as an explicit parameter.- Cache resolved
KPropertyandKFunctionobjects — re-resolving on every call is very expensive. Use aMaporConcurrentHashMapas a cache for hot paths.- Reflection is 10-100x slower than direct access. Don’t use it in inner loops or very frequently executed code. Profile before optimizing.
sealedSubclassesto programmatically get all subclasses of a sealed class — useful for tools, automatic documentation, or test case generation.- Prefer interfaces and generics over reflection — if you know the type at compile time, always use direct access. Reflection is only for cases where the type is truly unknown at compile time.
- Reflection is most appropriate inside libraries and frameworks, not in ordinary application code. If you’re using reflection in business logic, ask yourself: is there a more type-safe way to achieve the same thing?