1. What is reflection and when you need it
Imagine you are writing a program where it is impossible to list all types and scenarios in advance: plugins, serialization, a DI container, automatic log generation, import/export, universal diagnostics. In all such cases you want to be able to ask the program: “Who are you, exactly?” — and get an answer.
Reflection is exactly about that: code that can look at other code (and at types) at runtime. It sounds like science fiction, but in practice it is a tool that either helps a lot, or very quickly turns a project into “dynamic mush” if you use it without discipline.
Static vs runtime: two ways of thinking
In normal Kotlin you act on the principle: the compiler checked everything, so you can safely call methods. If you have an Expense, you write expense.amount, and the compiler guarantees that amount exists and has the right type.
With reflection you take a step aside: some decisions are made after the program starts. That means fewer compiler guarantees and more checks. A good rule of thumb is: if a task can be solved statically (with normal code) — solve it statically. Reflection is for places where a static approach becomes inconvenient or impossible, and you are willing to pay for the extra complexity and validation.
A small diagram to switch your brain:
flowchart LR
A[Regular Kotlin code] --> B[Compiler checked types]
B --> C[Run: just execute]
D[Reflection] --> E[Run: discover type/members]
E --> F[Validate that we found what we need]
F --> G[Only then execute]
2. KClass — the entry point to reflection
When you say “type” in Kotlin, you usually think of words like String, Int, Expense. But at runtime the program needs an object that represents this type. In Java it is Class<?>. In Kotlin it is KClass.
KClass is an object description of a class (and more broadly, of a type) that you can treat as data: compare it, read its name, use it for diagnostics, and (in the next lectures) look up properties/methods and even invoke them.
Important: KClass is the starting point. Today our task is to learn how to obtain a KClass and do the safest “without fanaticism” operations with it.
How to get a KClass
There are two most useful ways to get a KClass, and they look almost the same — as if Kotlin intentionally didn’t want you to relax.
When the type is known in advance: SomeType::class
Here you are saying: “I know the type at compile time, just give me its KClass”.
import kotlin.reflect.KClass
data class Expense(val id: Int, val amount: Int, val category: String)
fun main() {
val cls: KClass<Expense> = Expense::class
println(cls) // class Expense
}
Here Expense::class has the type KClass<Expense>. This is convenient when you build tables/maps “by types”, register handlers, or just want neat diagnostics.
When you have an object: obj::class
This approach is especially useful when an object comes in as a parameter typed as Any or a base interface, and you want to know its real runtime type.
data class Expense(val id: Int, val amount: Int, val category: String)
fun main() {
val x: Any = Expense(1, 500, "food")
println(x::class.simpleName) // Expense
}
Here x::class is the “passport” of the real object, not what is written on the left of =.
A nuance about simpleName
simpleName is convenient, but it is nullable: anonymous objects and some local classes may not have a name. So a careful style is to always keep a fallback:
fun typeLabel(x: Any): String {
return x::class.simpleName ?: "<no name>"
}
fun main() {
val obj = object { val x = 1 }
println(typeLabel(obj)) // <no name>
}
What you can do with KClass without heavy reflection
For now we are not touching memberFunctions/memberProperties (that will be the next lecture). But even at a minimal level, KClass is useful.
Diagnostics: print the type name in logs
If you build a console app, “smart” error and diagnostics messages are half the battle. KClass helps you write messages like “we got the wrong thing”.
fun describe(x: Any): String {
val name = x::class.simpleName ?: "<unknown>"
return "Received an object of type: $name"
}
fun main() {
println(describe("hi")) // Received an object of type: String
println(describe(123)) // Received an object of type: Int
}
Comparing types via KClass
Sometimes you want to say: “if this is exactly a string — do one thing, otherwise do another”. Then you can compare KClass:
fun isExactlyString(x: Any): Boolean {
return x::class == String::class
}
fun main() {
println(isExactlyString("ok")) // true
println(isExactlyString(10)) // false
}
Type table: Map<KClass<*>, ...>
Sometimes it is convenient to build a small reference table by type. Note KClass<*>: this is “a class of an unknown type”, a safe form when we do not care about the parameter.
import kotlin.reflect.KClass
fun main() {
val labels: Map<KClass<*>, String> = mapOf(
String::class to "string",
Int::class to "integer",
Double::class to "floating-point number"
)
val x: Any = 42
println(labels[x::class] ?: "something mysterious") // integer
}
This technique often appears in “dispatching” logic and in diagnostics.
is and ::class == ... are not the same thing
A very common trap is to think that the check x is Animal is equal to x::class == Animal::class. It is not. And the difference is fundamental: is considers inheritance, while comparing KClass is a check of the exact runtime class.
Let’s see:
open class Animal
class Cat : Animal()
fun main() {
val a: Animal = Cat()
println(a is Animal) // true
println(a::class == Animal::class) // false
println(a::class == Cat::class) // true
}
In human terms: is is “is this an animal at all?”, while ::class == ... is “is this animal exactly of class Animal (no subclasses)?”.
To cement it, here is a small table:
| Check | Meaning | Considers inheritance |
|---|---|---|
|
“x can be safely treated as T” | yes |
|
“the real class of x is exactly T” | no |
3. KClass in a console project: smart diagnostics
Now we will carefully apply KClass in a “do not break the architecture” style. We will not build business logic on reflection. We will add something that is almost always useful: a diagnostic description of what is happening.
Let’s imagine (as in your practical project) that we have commands and a command parser. It is convenient to model commands via a sealed interface so the compiler helps you keep the full list.
sealed interface Command
data class AddExpense(val amount: Int, val category: String) : Command
data object ListExpenses : Command
data class Help(val topic: String?) : Command
Command name for logs
Now let’s make a function that returns the “command name” for logs/debug output:
fun commandName(cmd: Command): String {
return cmd::class.simpleName ?: "<anonymous command>"
}
fun main() {
val cmd: Command = AddExpense(500, "food")
println(commandName(cmd)) // AddExpense
}
It is a small thing, but surprisingly pleasant: when you have a complex pipeline “input → parsing → execution → result”, this kind of diagnostics helps you avoid drowning in your own code.
Log an unexpected result type
If you return command execution results via a sealed interface, you can log which exact result came in, without printing the whole object.
sealed interface CommandResult {
data class Ok(val message: String) : CommandResult
data class Error(val message: String) : CommandResult
}
fun resultKind(r: CommandResult): String =
r::class.simpleName ?: "<unknown result>"
fun main() {
val r: CommandResult = CommandResult.Error("Bad input")
println(resultKind(r)) // Error
}
Again: this is not “magic”, but a careful improvement to readability and debugging.
4. When you need kotlin-reflect
Kotlin has an important practical nuance: full reflection is a separate library kotlin-reflect, and it is not always included automatically. For small programs this is a plus: fewer dependencies, faster startup, smaller size.
On the JVM, the compiler even has a separate option “do not add reflect to the classpath”. In the compiler options documentation there is a flag -no-reflect, which disables automatic inclusion of kotlin-reflect.jar.
Why this matters for you today: ::class and basic operations (like comparison and names) can be used almost always. But as soon as you want “a list of class members” (members) or access to properties/methods — you will usually need kotlin-reflect.
An example of accessing members appears even in the standard documentation on reified, where T::class.members is used.
We intentionally do not go deeper into this now: our goal is to learn the entry point (KClass) and not turn the project into a detective story of “why it doesn’t compile without one more dependency”.
5. KClass and Java Class: how to convert types
Kotlin on the JVM lives next to Java, so you often need to “translate” a Kotlin type to a Java type and back. There are very straightforward bridges for this.
If you have a KClass, the Java class can usually be obtained via .java:
fun main() {
val javaCls = String::class.java
println(javaCls.name) // java.lang.String
}
And if you have an object, you can get the Java class via javaClass:
fun main() {
val x = "hi"
println(x.javaClass.name) // java.lang.String
}
This is especially useful when you use Java APIs that require Class<T> (for example, some factories, registries, old DI tools, and so on). But it is still more convenient to start thinking from the Kotlin side: ::class and KClass.
6. Common mistakes when getting familiar with KClass
Mistake №1: confusing is and ::class == ...::class.
This is the most common logical trap. The is check answers “can the object be considered of type T?” and considers inheritance. Comparing KClass checks the exact class. If you mix them up, you will get strange branching: the code “doesn’t recognize” subclasses and behaves as if polymorphism was canceled.
Mistake №2: using simpleName!! and getting an NPE for no good reason.
For anonymous objects, local classes, and some special cases, the name may be unavailable, so simpleName is nullable. In diagnostics and logs it is better to keep a fallback like ?: "<no name>" than to crash inside the logging system one day.
Mistake №3: assuming reflection is always available, then being surprised that members doesn’t work.
Basic things with KClass are available almost always, but advanced reflection usually requires kotlin-reflect. If the project is built without it (or with a flag that prevents pulling it in automatically), some capabilities will be unavailable. The -no-reflect flag in JVM compiler options is exactly about that.
Mistake №4: building business logic on string type names “for flexibility”.
At the beginning it seems that “if I decide based on simpleName, it will be flexible”. In practice this turns into fragile code: you renamed a class — and the logic changed. If you use KClass, it is better to compare KClass directly, and keep strings for human-readable messages.
Mistake №5: doing reflection in a hot code path.
Even if today you only use ::class and simpleName, keep a simple rule in mind: reflection is not free. If you start doing it inside large loops and collection processing “for every record”, you may unexpectedly hit performance limits. For now this is just a warning for the future, not a reason to panic.
GO TO FULL VERSION