Kotlin - Reflection
Try it: Use Kotlin's reflection API to analyze classes at runtime and build flexible applications.
What is Reflection?
Reflection allows you to examine and manipulate program structure at runtime. Kotlin provides interoperability with Java reflection plus its own reflection API.
Basic Class Reflection
import kotlin.reflect.KClass
import kotlin.reflect.full.*
class User(val name: String, val age: Int) {
fun greet() = "Hello, I'm $name"
}
fun main() {
// Get class reference
val userClass: KClass<User> = User::class
// Class information
println("Class name: ${userClass.simpleName}")
println("Qualified name: ${userClass.qualifiedName}")
// Check if class is data class
println("Is data class: ${userClass.isData}")
// Get constructors
userClass.constructors.forEach { constructor ->
println("Constructor parameters: ${constructor.parameters.map { it.name }}")
}
}
// Output:
// Class name: User
// Qualified name: User
// Is data class: false
// Constructor parameters: [name, age]
Property Reflection
import kotlin.reflect.full.*
data class Person(var firstName: String, var lastName: String) {
val fullName: String get() = "$firstName $lastName"
}
fun main() {
val person = Person("John", "Doe")
val personClass = Person::class
// Get all properties
personClass.memberProperties.forEach { property ->
println("Property: ${property.name}, Type: ${property.returnType}")
// Get property value
val value = property.get(person)
println("Value: $value")
// Check if mutable
if (property is kotlin.reflect.KMutableProperty1) {
println("Property is mutable")
}
}
// Access specific property
val firstNameProperty = personClass.memberProperties
.first { it.name == "firstName" }
// Modify mutable property
if (firstNameProperty is kotlin.reflect.KMutableProperty1) {
firstNameProperty.set(person, "Jane")
println("Updated: ${person.firstName}")
}
}
Function Reflection
import kotlin.reflect.full.*
class Calculator {
fun add(a: Int, b: Int): Int = a + b
fun multiply(x: Double, y: Double): Double = x * y
private fun secret() = "Hidden"
}
fun main() {
val calc = Calculator()
val calcClass = Calculator::class
// Get all functions
calcClass.memberFunctions.forEach { function ->
println("Function: ${function.name}")
println("Parameters: ${function.parameters.drop(1).map { "${it.name}: ${it.type}" }}")
println("Return type: ${function.returnType}")
println("Visibility: ${function.visibility}")
println()
}
// Call function by name
val addFunction = calcClass.memberFunctions
.first { it.name == "add" }
val result = addFunction.call(calc, 5, 3)
println("Dynamic call result: $result")
}
Annotation Reflection
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ApiEndpoint(val path: String, val method: String = "GET")
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonProperty(val name: String)
@ApiEndpoint("/users", "POST")
data class UserRequest(
@JsonProperty("user_name") val username: String,
@JsonProperty("user_email") val email: String
) {
@ApiEndpoint("/validate", "POST")
fun validate(): Boolean = username.isNotEmpty() && email.contains("@")
}
fun main() {
val userClass = UserRequest::class
// Class annotations
userClass.annotations.forEach { annotation ->
when (annotation) {
is ApiEndpoint -> {
println("API: ${annotation.method} ${annotation.path}")
}
}
}
// Property annotations
userClass.memberProperties.forEach { property ->
property.annotations.forEach { annotation ->
when (annotation) {
is JsonProperty -> {
println("Property ${property.name} maps to JSON field '${annotation.name}'")
}
}
}
}
// Function annotations
userClass.memberFunctions.forEach { function ->
function.annotations.forEach { annotation ->
when (annotation) {
is ApiEndpoint -> {
println("Function ${function.name}: ${annotation.method} ${annotation.path}")
}
}
}
}
}
Generic Type Reflection
import kotlin.reflect.full.*
import kotlin.reflect.typeOf
class Container<T>(val value: T) {
fun getValue(): T = value
}
inline fun <reified T> analyzeType() {
val type = typeOf<T>()
println("Type: $type")
println("Classifier: ${type.classifier}")
println("Arguments: ${type.arguments}")
}
fun main() {
// Analyze different generic types
analyzeType<List<String>>()
analyzeType<Map<String, Int>>()
analyzeType<Container<Person>>()
// Runtime type checking
val stringList: List<String> = listOf("a", "b", "c")
val listType = typeOf<List<String>>()
println("Type matches: ${stringList::class.createType() == listType}")
}
Practical Applications
JSON Serialization Framework
import kotlin.reflect.full.*
@Target(AnnotationTarget.PROPERTY)
annotation class JsonIgnore
@Target(AnnotationTarget.PROPERTY)
annotation class JsonName(val name: String)
object SimpleJsonSerializer {
fun serialize(obj: Any): String {
val kClass = obj::class
val properties = kClass.memberProperties
val jsonFields = properties
.filter { !it.hasAnnotation<JsonIgnore>() }
.map { property ->
val jsonName = property.findAnnotation<JsonName>()?.name ?: property.name
val value = property.get(obj)
val jsonValue = when (value) {
is String -> "\"$value\""
is Number -> value.toString()
is Boolean -> value.toString()
null -> "null"
else -> serialize(value) // Recursive for nested objects
}
"\"$jsonName\": $jsonValue"
}
return "{${jsonFields.joinToString(", ")}}"
}
}
data class Address(val street: String, val city: String)
data class Employee(
@JsonName("emp_name") val name: String,
val salary: Double,
@JsonIgnore val internalId: String,
val address: Address
)
fun main() {
val emp = Employee(
name = "John Doe",
salary = 75000.0,
internalId = "INTERNAL_123",
address = Address("123 Main St", "Springfield")
)
println(SimpleJsonSerializer.serialize(emp))
}
Configuration System
import kotlin.reflect.full.*
@Target(AnnotationTarget.PROPERTY)
annotation class ConfigProperty(val key: String, val defaultValue: String = "")
@Target(AnnotationTarget.PROPERTY)
annotation class Required
class DatabaseConfig {
@ConfigProperty("db.host", "localhost")
var host: String = ""
@ConfigProperty("db.port", "5432")
var port: Int = 0
@ConfigProperty("db.username")
@Required
var username: String = ""
@ConfigProperty("db.password")
@Required
var password: String = ""
@ConfigProperty("db.pool.size", "10")
var poolSize: Int = 0
}
class ConfigLoader {
fun load<T : Any>(configClass: KClass<T>, properties: Map<String, String>): T {
val instance = configClass.java.getDeclaredConstructor().newInstance()
configClass.memberProperties.forEach { property ->
val configAnnotation = property.findAnnotation<ConfigProperty>()
if (configAnnotation != null) {
val value = properties[configAnnotation.key] ?: configAnnotation.defaultValue
// Check if required
if (property.hasAnnotation<Required>() && value.isEmpty()) {
throw IllegalStateException("Required property ${configAnnotation.key} is missing")
}
// Set value with type conversion
if (property is kotlin.reflect.KMutableProperty1 && value.isNotEmpty()) {
val convertedValue = when (property.returnType.classifier) {
String::class -> value
Int::class -> value.toInt()
Double::class -> value.toDouble()
Boolean::class -> value.toBoolean()
else -> value
}
property.set(instance, convertedValue)
}
}
}
return instance
}
}
fun main() {
val properties = mapOf(
"db.host" to "prod-db.company.com",
"db.username" to "app_user",
"db.password" to "secret123",
"db.pool.size" to "20"
)
val loader = ConfigLoader()
val config = loader.load(DatabaseConfig::class, properties)
println("Database Config:")
println("Host: ${config.host}")
println("Port: ${config.port}")
println("Username: ${config.username}")
println("Pool Size: ${config.poolSize}")
}
Dependency Injection Container
import kotlin.reflect.KClass
import kotlin.reflect.full.*
@Target(AnnotationTarget.CONSTRUCTOR)
annotation class Inject
@Target(AnnotationTarget.CLASS)
annotation class Singleton
interface Logger {
fun log(message: String)
}
@Singleton
class ConsoleLogger : Logger {
override fun log(message: String) {
println("[LOG] $message")
}
}
interface UserRepository {
fun findUser(id: String): String?
}
class DatabaseUserRepository @Inject constructor(
private val logger: Logger
) : UserRepository {
override fun findUser(id: String): String? {
logger.log("Finding user with id: $id")
return "User_$id"
}
}
class SimpleContainer {
private val singletons = mutableMapOf<KClass<*>, Any>()
private val bindings = mutableMapOf<KClass<*>, KClass<*>>()
fun <T : Any> bind(interfaceClass: KClass<T>, implementationClass: KClass<out T>) {
bindings[interfaceClass] = implementationClass
}
fun <T : Any> get(clazz: KClass<T>): T {
// Check if singleton already exists
if (singletons.containsKey(clazz)) {
@Suppress("UNCHECKED_CAST")
return singletons[clazz] as T
}
// Find implementation class
val implementationClass = bindings[clazz] ?: clazz
// Find injectable constructor
val constructor = implementationClass.constructors.find {
it.hasAnnotation<Inject>()
} ?: implementationClass.constructors.first()
// Resolve dependencies
val args = constructor.parameters.map { param ->
@Suppress("UNCHECKED_CAST")
get(param.type.classifier as KClass<Any>)
}
// Create instance
val instance = constructor.call(*args.toTypedArray())
// Store singleton if annotated
if (implementationClass.hasAnnotation<Singleton>()) {
singletons[clazz] = instance
}
@Suppress("UNCHECKED_CAST")
return instance as T
}
}
fun main() {
val container = SimpleContainer()
// Configure bindings
container.bind(Logger::class, ConsoleLogger::class)
container.bind(UserRepository::class, DatabaseUserRepository::class)
// Resolve dependencies
val userRepo = container.get(UserRepository::class)
val result = userRepo.findUser("123")
println("Result: $result")
// Verify singleton behavior
val logger1 = container.get(Logger::class)
val logger2 = container.get(Logger::class)
println("Same logger instance: ${logger1 === logger2}")
}
Common Pitfalls
- Performance: Reflection is slower than direct access; cache reflection objects when possible
- Security: Reflection can access private members; be cautious with user input
- Type Safety: Reflection operations are not type-safe at compile time; use carefully
- Proguard/R8: Code obfuscation can break reflection; configure keep rules appropriately
Best Practices
- Use inline functions with reified type parameters when possible instead of reflection
- Cache KClass and KFunction objects to avoid repeated reflection calls
- Prefer sealed classes and when expressions over reflection for type checking
- Use annotations with retention RUNTIME only when necessary
- Consider compile-time code generation alternatives like KSP (Kotlin Symbol Processing)
Practice Exercises
- Create a validation framework that uses annotations to validate object properties
- Build a simple ORM that maps objects to SQL queries using reflection
- Implement a command pattern dispatcher that routes method calls based on annotations
Architecture Notes
- Framework Design: Reflection enables building flexible frameworks and libraries
- Configuration: Useful for dynamic configuration loading and dependency injection
- Serialization: Powers JSON/XML serialization libraries
- Testing: Enables accessing private members for testing purposes