Skip to main content

Kotlin Reflection

Introduction

Reflection is a powerful feature in programming languages that enables programs to inspect and manipulate themselves at runtime. It allows you to examine classes, interfaces, functions, and properties programmatically, which can be extremely useful for creating flexible and dynamic applications.

Kotlin provides a rich reflection API that allows you to:

  • Examine class structures
  • Access properties and call functions dynamically
  • Create new instances of classes
  • Extend functionality with annotations
  • Build powerful frameworks and libraries

In this guide, we'll explore Kotlin's reflection capabilities and learn how to use them effectively in your applications.

Understanding the Kotlin Reflection API

Kotlin's reflection API is available in the kotlin.reflect package. To use reflection in your projects, you'll need to include the Kotlin reflection library as a dependency:

kotlin
// In Gradle (Kotlin DSL)
dependencies {
implementation(kotlin("reflect"))
}

The API consists of several main components:

  • KClass: Represents a class and provides access to its members
  • KFunction: Represents a function (method)
  • KProperty: Represents a property (field)
  • KParameter: Represents a parameter of a callable
  • KType: Represents a type

Let's dive into each of these components with examples.

Getting Class References

In Kotlin, there are several ways to obtain a reference to a class:

kotlin
// Class reference using ::class
val stringClass = String::class
println(stringClass) // class kotlin.String

// From an instance using ::class
val example = "Hello, reflection!"
val instanceClass = example::class
println(instanceClass) // class kotlin.String

// For Java class reference
val javaStringClass = String::class.java
println(javaStringClass) // class java.lang.String

Output:

class kotlin.String
class kotlin.String
class java.lang.String

Exploring Class Members

Once you have a class reference, you can examine its properties, functions, and other members:

kotlin
class Person(val name: String, var age: Int) {
fun introduce() = "Hi, I'm $name and I'm $age years old."
private fun secretMethod() = "This is private!"
}

fun examineClass() {
val personClass = Person::class

// List all properties
println("Properties:")
personClass.memberProperties.forEach {
println(" - ${it.name}: ${it.returnType}")
}

// List all functions
println("\nFunctions:")
personClass.memberFunctions.forEach {
println(" - ${it.name}${it.parameters}")
}

// Get constructors
println("\nConstructors:")
personClass.constructors.forEach {
println(" - ${it.parameters}")
}
}

Output:

Properties:
- age: kotlin.Int
- name: kotlin.String

Functions:
- equals[parameter #0 other: kotlin.Any?]
- hashCode[]
- introduce[]
- secretMethod[]
- toString[]

Constructors:
- [parameter #0 name: kotlin.String, parameter #1 age: kotlin.Int]

Working with Properties

Kotlin reflection allows you to get and set property values dynamically:

kotlin
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.memberProperties

class User(val id: Int, var name: String, var email: String)

fun workWithProperties() {
val user = User(1, "Alex", "[email protected]")
val userClass = User::class

// Read properties dynamically
println("Original user properties:")
userClass.memberProperties.forEach { prop ->
println(" - ${prop.name}: ${prop.getter.call(user)}")
}

// Modify mutable properties
userClass.memberProperties.forEach { prop ->
if (prop is KMutableProperty<*> && prop.name == "name") {
prop.setter.call(user, "Alexander")
}
}

println("\nAfter modification:")
userClass.memberProperties.forEach { prop ->
println(" - ${prop.name}: ${prop.getter.call(user)}")
}
}

Output:

Original user properties:
- id: 1
- name: Alex
- email: [email protected]

After modification:
- id: 1
- name: Alexander
- email: [email protected]

Calling Functions Dynamically

You can also invoke functions dynamically using reflection:

kotlin
import kotlin.reflect.full.functions
import kotlin.reflect.full.findFunction

class Calculator {
fun add(a: Int, b: Int) = a + b
fun multiply(a: Int, b: Int) = a * b
fun greet(name: String) = "Hello, $name!"
}

fun callFunctions() {
val calculator = Calculator()
val calculatorClass = Calculator::class

// Find and call a function by name
val addFunction = calculatorClass.functions.find { it.name == "add" }
val multiplyFunction = calculatorClass.functions.find { it.name == "multiply" }
val greetFunction = calculatorClass.functions.find { it.name == "greet" }

if (addFunction != null && multiplyFunction != null && greetFunction != null) {
val sum = addFunction.call(calculator, 5, 3)
val product = multiplyFunction.call(calculator, 5, 3)
val greeting = greetFunction.call(calculator, "Reflection")

println("5 + 3 = $sum")
println("5 * 3 = $product")
println(greeting)
}
}

Output:

5 + 3 = 8
5 * 3 = 15
Hello, Reflection!

Creating Instances Dynamically

Reflection allows you to create new instances of classes dynamically:

kotlin
import kotlin.reflect.full.primaryConstructor

class Product(val id: String, val name: String, val price: Double)

fun createInstances() {
val productClass = Product::class

// Get the primary constructor
val constructor = productClass.primaryConstructor

if (constructor != null) {
// Create new instances with different parameters
val product1 = constructor.call("P001", "Laptop", 999.99)
val product2 = constructor.call("P002", "Smartphone", 599.99)

println("Product 1: ${product1.id}, ${product1.name}, $${product1.price}")
println("Product 2: ${product2.id}, ${product2.name}, $${product2.price}")
}
}

Output:

Product 1: P001, Laptop, $999.99
Product 2: P002, Smartphone, $599.99

Working with Annotations

Annotations are particularly useful with reflection, allowing you to add metadata to your code:

kotlin
import kotlin.reflect.full.findAnnotation

@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
annotation class ApiEntity(val endpoint: String)

@Target(AnnotationTarget.PROPERTY)
annotation class ApiField(val name: String, val required: Boolean = false)

@ApiEntity("/users")
class ApiUser(
@ApiField("user_id", required = true)
val id: Int,

@ApiField("user_name", required = true)
val name: String,

@ApiField("user_email")
val email: String
)

fun workWithAnnotations() {
val userClass = ApiUser::class

// Get class annotation
val apiEntity = userClass.findAnnotation<ApiEntity>()
println("API Endpoint: ${apiEntity?.endpoint}")

// Get property annotations
println("\nAPI Fields:")
userClass.members.forEach { member ->
val apiField = member.findAnnotation<ApiField>()
if (apiField != null) {
println(" - ${member.name}: field name='${apiField.name}', required=${apiField.required}")
}
}
}

Output:

API Endpoint: /users

API Fields:
- id: field name='user_id', required=true
- name: field name='user_name', required=true
- email: field name='user_email', required=false

Practical Application: Simple Dependency Injection Framework

Let's create a simple dependency injection framework using reflection to demonstrate a real-world application:

kotlin
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.full.findAnnotation

// Annotations
@Target(AnnotationTarget.CLASS)
annotation class Injectable

@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.PROPERTY)
annotation class Inject

// Our simple DI container
class DIContainer {
private val instances = mutableMapOf<KClass<*>, Any>()

fun <T : Any> register(instance: T) {
instances[instance::class] = instance
}

@Suppress("UNCHECKED_CAST")
fun <T : Any> get(kClass: KClass<T>): T {
// Return existing instance if available
instances[kClass]?.let { return it as T }

// Check if class is injectable
if (kClass.findAnnotation<Injectable>() == null) {
throw IllegalArgumentException("Class ${kClass.simpleName} is not marked as @Injectable")
}

// Create new instance using constructor
val constructor = kClass.primaryConstructor
?: throw IllegalArgumentException("Class ${kClass.simpleName} has no primary constructor")

// Resolve constructor parameters
val parameters = constructor.parameters.map { param ->
val paramType = param.type.classifier as? KClass<*>
?: throw IllegalArgumentException("Cannot resolve parameter type")

get(paramType)
}

// Create instance
val instance = constructor.call(*parameters.toTypedArray())

// Store and return
instances[kClass] = instance
return instance
}

inline fun <reified T : Any> get(): T = get(T::class)
}

// Example classes
@Injectable
class Logger {
fun log(message: String) = println("LOG: $message")
}

@Injectable
class Database(private val logger: Logger) {
fun query(sql: String): String {
logger.log("Executing query: $sql")
return "Result for: $sql"
}
}

@Injectable
class UserService(private val database: Database, private val logger: Logger) {
fun getUser(id: Int): String {
logger.log("Fetching user with ID: $id")
return database.query("SELECT * FROM users WHERE id = $id")
}
}

fun demonstrateDI() {
val container = DIContainer()

// Get service (DI container will resolve and inject dependencies automatically)
val userService = container.get<UserService>()

// Use the service
val result = userService.getUser(42)
println("Result: $result")
}

Output:

LOG: Fetching user with ID: 42
LOG: Executing query: SELECT * FROM users WHERE id = 42
Result: Result for: SELECT * FROM users WHERE id = 42

Performance Considerations

While reflection is powerful, it does come with performance overhead. Here are some best practices:

  1. Cache reflection results: Store references to KClass objects, properties, and functions if you'll use them repeatedly.
  2. Avoid reflection in performance-critical code: Use it for configuration, setup, or in areas where performance is less critical.
  3. Consider alternatives: For simple cases, Kotlin's delegation, higher-order functions, or interfaces might be more efficient solutions.
kotlin
// Example of caching reflection results
class ReflectionCache<T : Any>(private val kClass: KClass<T>) {
val properties by lazy { kClass.memberProperties.toList() }
val functions by lazy { kClass.functions.toList() }

fun getProperty(name: String) = properties.find { it.name == name }
fun getFunction(name: String) = functions.find { it.name == name }
}

// Usage
val personCache = ReflectionCache(Person::class)

Summary

Kotlin's reflection API offers powerful tools for introspecting and manipulating code at runtime. In this guide, we've explored:

  • Getting class references and examining their structure
  • Working with properties and functions dynamically
  • Creating instances through reflection
  • Using annotations with reflection
  • Building a simple dependency injection framework

Reflection enables you to write more flexible and dynamic code, though it should be used judiciously due to its performance impact and the loss of some compile-time safety guarantees.

Exercises

  1. Create a simple object-to-JSON serializer using reflection
  2. Build a test framework that automatically discovers and runs methods annotated with @Test
  3. Implement a simple ORM (Object-Relational Mapping) that uses reflection to map objects to database tables
  4. Create an annotation-based validation library for data classes
  5. Extend the DI container example with support for interface bindings

Additional Resources

With reflection, you've unlocked another powerful tool in your Kotlin programming arsenal. While it should be used thoughtfully, reflection can help you create more flexible and elegant solutions to complex problems.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)