Metaprogramming
Reflection
Section titled “Reflection”Reflection allows inspection of classes, functions, properties, and annotations at runtime. Kotlin Reflection requires an additional dependency.
dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect:2.1.0")}Class References
Section titled “Class References”val kClass: KClass<String> = String::classval jClass: Class<String> = String::class.javaval runtimeClass: KClass<out String> = "hello"::classProperty References
Section titled “Property References”data class User(val name: String, var age: Int)
val nameProperty = User::nameprintln(nameProperty.name) // "name"println(nameProperty.get(User("A", 1))) // "A"
val ageProperty = User::ageval user = User("A", 1)ageProperty.set(user, 25)Function References
Section titled “Function References”fun isEven(n: Int): Boolean = n % 2 == 0
val predicate: (Int) -> Boolean = ::isEvenlistOf(1, 2, 3, 4).filter(::isEven) // [2, 4]Callable References on Instances
Section titled “Callable References on Instances”val user = User("Alice", 30)val nameGetter: () -> String = user::nameval ageSetter: (Int) -> Unit = user::age::setIntrospection
Section titled “Introspection”data class Config(val host: String, val port: Int, val debug: Boolean)
val kClass = Config::class
kClass.memberProperties.forEach { prop -> println("${prop.name}: ${prop.returnType}")}
kClass.primaryConstructor?.parameters?.forEach { param -> println("${param.name}: ${param.type}")}Dynamic Invocation
Section titled “Dynamic Invocation”fun setProperty(obj: Any, propertyName: String, value: Any?) { val prop = obj::class.memberProperties.firstOrNull { it.name == propertyName } (prop as? KMutableProperty1<Any, Any?>)?.set(obj, value)}
val config = Config("localhost", 8080, false)setProperty(config, "debug", true)Reflection has significant performance overhead compared to direct access. Use it for framework code (serialization, dependency injection, ORM) where the structure is not known at compile time.
Annotations
Section titled “Annotations”Annotations attach metadata to declarations. They do not directly affect program behavior but can be Read via reflection or used by the compiler.
Declaration
Section titled “Declaration”annotation class ApiEndpoint(val method: String, val path: String)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)@Retention(AnnotationRetention.RUNTIME)@MustBeDocumentedannotation class Deprecated(val message: String, val replaceWith: String = "")Built-in Annotation Targets
Section titled “Built-in Annotation Targets”| Target | Applies To |
|---|---|
AnnotationTarget.CLASS | Classes, interfaces, objects, annotations |
AnnotationTarget.FUNCTION | Functions (including constructors) |
AnnotationTarget.PROPERTY | Properties |
AnnotationTarget.VALUE_PARAMETER | Function/constructor parameters |
AnnotationTarget.FIELD | Backing fields |
AnnotationTarget.TYPE | Types in type usages |
Retention
Section titled “Retention”| Retention | Behavior |
|---|---|
SOURCE | Discarded by the compiler (not in bytecode) |
BINARY | Stored in bytecode but not visible via reflection |
RUNTIME | Stored in bytecode and accessible via reflection |
@ApiEndpoint(method = "GET", path = "/users/{id}")fun getUser(@Path("id") id: Long): User { // ...}Annotation Parameters
Section titled “Annotation Parameters”Annotation parameters are restricted to: primitive types, strings, classes, enums, other Annotations, and arrays of these types.
annotation class Validate( val min: Int = 0, val max: Int = Int.MAX_VALUE, val message: String, val groups: Array<KClass<*>> = [])
@Validate(min = 1, max = 100, message = "Age must be between 1 and 100")var age: Int = 0Meta-Annotations
Section titled “Meta-Annotations”@Target(AnnotationTarget.ANNOTATION_CLASS)annotation class Validator
@Validatorannotation class MinLength(val value: Int)Sealed Interfaces
Section titled “Sealed Interfaces”Kotlin 1.5+ supports sealed interfaces, enabling closed type hierarchies without requiring a common Base class.
sealed interface Message { val timestamp: Long}
data class TextMessage( override val timestamp: Long, val content: String) : Message
data class ImageMessage( override val timestamp: Long, val url: String, val altText: String?) : Message
data class SystemMessage( override val timestamp: Long, val event: String) : MessageSealed interfaces enable exhaustive when expressions, just like sealed classes.
fun render(message: Message): String = when (message) { is TextMessage -> "[${message.timestamp}] ${message.content}" is ImageMessage -> "[${message.timestamp}] Image: ${message.altText ?: message.url}" is SystemMessage -> "[${message.timestamp}] System: ${message.event}"}A type can both extend a sealed class and implement a sealed interface:
sealed class Result<out T>sealed interface Succeeded
data class Success<T>(val value: T) : Result<T>(), SucceededValue Classes (Inline Classes)
Section titled “Value Classes (Inline Classes)”Value classes provide a way to wrap a value with a distinct type without the runtime overhead of Object allocation.
@JvmInlinevalue class UserId(val value: Long)
@JvmInlinevalue class Email(val value: String) { init { require(value.contains("@")) { "Invalid email" } }
val domain: String get() = value.substringAfter("@")}At runtime, UserId is represented as a plain Long. No object allocation occurs.
Restrictions
Section titled “Restrictions”- Must have exactly one property in the primary constructor.
- Cannot have
initblocks that access the backing property before it is initialized (validation ininitblocks is allowed). - Cannot extend other classes (but can implement interfaces).
- Cannot be used as generic type arguments at runtime (they are erased).
Interfaces
Section titled “Interfaces”interface Identifiable { val id: String}
@JvmInlinevalue class OrderId(val value: String) : Identifiable { override val id: String get() = value}Equality and Identity
Section titled “Equality and Identity”Value classes use structural equality based on the wrapped value.
val a = UserId(42)val b = UserId(42)println(a == b) // trueprintln(a === b) // false (identity check not meaningful for value classes)Operator Overloading
Section titled “Operator Overloading”Kotlin allows overloading a fixed set of operators by defining functions with specific names.
Arithmetic Operators
Section titled “Arithmetic Operators”data class Vec2(val x: Double, val y: Double) { operator fun plus(other: Vec2) = Vec2(x + other.x, y + other.y) operator fun minus(other: Vec2) = Vec2(x - other.x, y - other.y) operator fun times(scalar: Double) = Vec2(x * scalar, y * scalar) operator fun unaryMinus() = Vec2(-x, -y)}
val a = Vec2(1.0, 2.0)val b = Vec2(3.0, 4.0)val c = a + b // Vec2(4.0, 6.0)val d = -a // Vec2(-1.0, -2.0)Comparison Operators
Section titled “Comparison Operators”data class Version(val major: Int, val minor: Int, val patch: Int) : Comparable<Version> { override fun compareTo(other: Version): Int = compareValuesBy( this, other, { it.major }, { it.minor }, { it.patch } )}
val v1 = Version(2, 1, 0)val v2 = Version(2, 3, 0)println(v1 < v2) // trueIndexing Operators
Section titled “Indexing Operators”class Matrix(private val data: DoubleArray, val rows: Int, val cols: Int) { operator fun get(row: Int, col: Int): Double = data[row * cols + col] operator fun set(row: Int, col: Int, value: Double) { data[row * cols + col] = value }}
val m = Matrix(DoubleArray(9), 3, 3)m[0, 0] = 1.0println(m[0, 0]) // 1.0In Operator
Section titled “In Operator”operator fun Version.rangeTo(other: Version): ClosedRange<Version> { return VersionRange(this, other)}
for (v in Version(1, 0, 0)..Version(2, 0, 0)) { println(v)}Invoke Operator
Section titled “Invoke Operator”class Config { operator fun invoke(block: Config.() -> Unit): Config { this.block() return this }}
val config = Config().invoke { // configure}Available Operators
Section titled “Available Operators”| Expression | Operator Function | Translation |
|---|---|---|
a + b | a.plus(b) | Binary plus |
a - b | a.minus(b) | Binary minus |
a * b | a.times(b) | Multiplication |
a / b | a.div(b) | Division |
a % b | a.rem(b) | Remainder |
-a | a.unaryMinus() | Unary minus |
++a / a++ | a.inc() | Increment |
a > b | a.compareTo(b) > 0 | Comparison |
a in b | b.contains(a) | Contains |
a[i] | a.get(i) | Indexing |
a(i) | a.invoke(i) | Function call |
a..b | a.rangeTo(b) | Range |
Common Pitfalls
Section titled “Common Pitfalls”- ** Overusing reflection for tasks that can be solved with compile-time mechanisms. Reflection bypasses the type system and has significant performance cost. Prefer code generation, annotation processors, or inline functions.
- ** Using value classes where inheritance is needed. Value classes cannot extend other classes. If you need a type hierarchy, use a regular class or sealed class.
- ** Overloading operators in ways that violate the principle of least surprise. The
+operator should represent addition or concatenation, not arbitrary behavior. - ** Forgetting that
equals()andhashCode()are not generated for value classes implementing interfaces that declare them. The interface equality is used instead of structural equality. - ** Using reflection on value classes. Value classes are represented as their underlying type at runtime.
KClassfor a value class may not behave as expected — use the underlying type for reflection operations.
Summary
Section titled “Summary”This topic covers the core concepts of metaprogramming, including underlying theory, practical implementation, and key applications.
Key concepts include:
- variables, data types, and control flow
- functions and procedures
- object-oriented programming
- error handling and debugging
- modular design
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Intuition
Section titled “Intuition”Reflection is like having X-ray vision for your code — it lets you inspect the internal structure of classes, methods, and properties at runtime. Annotations are like labels you stick on your code, and reflection is the ability to read those labels and act on them. This is how frameworks like Spring and Hibernate work: they scan your classes for annotations and automatically wire up dependency injection or ORM mapping. The trade-off is performance: reflection is slower than direct access because the JVM must look up metadata dynamically. Use it for framework code where flexibility matters, and direct calls for performance-critical paths.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Types and Variables: Type system fundamentals that reflection inspects at runtime.
- Null Safety Deep Dive: Contracts and safe-call operators used in reflective null handling.
- Control Flow: When expressions and sealed class patterns that reflection can enumerate.