Metaprogramming | Kotlin - Wyatt's Notes
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.
flowchart TD
A[Metaprogramming] --> B[Key Concepts]
A --> C[Core Principles]
A --> D[Practical Applications]
B --> E[Fundamental definitions]
C --> F[Design patterns]
D --> G[Real-world usage]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.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Intuition
Section titled “Intuition”Reflection lets you inspect and manipulate types at runtime when compile-time information is insufficient. Kotlin’s reflection API uses KClass, KFunction, and KProperty to represent types, functions, and properties. Annotations attach metadata that frameworks read via reflection — they do not affect program behavior directly but enable configuration-driven development. Value classes wrap a single value with a distinct type that compiles away at runtime, providing type safety without allocation overhead. Operator overloading lets you define custom behavior for symbols like +, -, [], and () by implementing specific function names, making domain-specific code read like natural mathematical notation.
Cross-References
Section titled “Cross-References”- Classes and Objects — data classes and sealed classes
- Functions — extension functions and higher-order functions
- Coroutines — suspend functions and structured concurrency