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:
// In Gradle (Kotlin DSL)
dependencies {
implementation(kotlin("reflect"))
}
The API consists of several main components:
KClass
: Represents a class and provides access to its membersKFunction
: Represents a function (method)KProperty
: Represents a property (field)KParameter
: Represents a parameter of a callableKType
: 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:
// 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:
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:
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:
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:
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:
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:
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:
- Cache reflection results: Store references to KClass objects, properties, and functions if you'll use them repeatedly.
- Avoid reflection in performance-critical code: Use it for configuration, setup, or in areas where performance is less critical.
- Consider alternatives: For simple cases, Kotlin's delegation, higher-order functions, or interfaces might be more efficient solutions.
// 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
- Create a simple object-to-JSON serializer using reflection
- Build a test framework that automatically discovers and runs methods annotated with
@Test
- Implement a simple ORM (Object-Relational Mapping) that uses reflection to map objects to database tables
- Create an annotation-based validation library for data classes
- Extend the DI container example with support for interface bindings
Additional Resources
- Official Kotlin Reflection Documentation
- Kotlin Reflection API Reference
- Performance Tips for Kotlin Reflection
- Design Patterns Using Reflection
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! :)