Inline Functions & reified
Reviewed & published by Brayan K
An inline function is copied directly into each call site by the compiler, eliminating the object allocation a passed lambda would otherwise need and enabling reified type parameters that survive at runtime.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll learn why higher-order functions benefit from inline, how reified lets you check a generic type at runtime, and what noinline and crossinline are for.
What You'll Learn in This Lesson
- What the inline keyword does to a function
- Why inlining avoids per-call lambda allocation
- Why higher-order functions benefit most
- How reified keeps a generic type at runtime
- Writing is T and T::class with reified
- When noinline and crossinline are needed
1️⃣ What inline Does (and Why It Helps)
Marking a function inline asks the compiler to paste its body — and the body of any lambda you pass to it — directly where the function is called, instead of generating a real call. The program behaves identically; only the compiled bytecode changes. The payoff appears with higher-order functions: a passed lambda would normally become a heap-allocated object, and inlining removes that object entirely.
// 'inline' tells the compiler to copy this function's body — and the body
// of any lambda passed to it — directly into the call site. No function
// object is created for the lambda, so there is no per-call allocation.
inline fun measure(label: String, block: () -> Unit) {
println("start $label")
block()
println("end $label")
}
fun main() {
measure("task") {
println("doing work")
}
}For a one-off call the difference is invisible, but inside a loop that runs millions of times, removing the per-call lambda allocation is exactly why Kotlin's map, filter, and forEach are all declared inline.
2️⃣ reified: Knowing T at Runtime
On the JVM, generic types are normally erased — at runtime a generic function has no idea what T was, so item is T won't compile. Because an inline function is copied into each call site, the compiler can substitute the real type there. Marking the type parameter reified unlocks exactly that, letting you write is T and T::class.
// Normally a generic type T is ERASED at runtime — you cannot write 'is T'.
// Marking the function 'inline' and the type 'reified' keeps T available,
// so you CAN check against it at runtime.
inline fun <reified T> firstOfType(items: List<Any>): T? {
for (item in items) {
if (item is T) return item
}
return null
}
fun main() {
val mixed: List<Any> = listOf(1, "hello", 2.5, "world", 42)
// Find the first String, then the first Int:
val firstString: String? = firstOfType<String>(mixed)
val firstInt: Int? = firstOfType<Int>(mixed)
println(firstString) // -> hello
println(firstInt) // -> 1
}reified is only allowed on inline functions, because the type substitution happens precisely during the copy-into-call-site step.
3️⃣ A Real reified Helper (and noinline / crossinline)
The standard library's filterIsInstance<T>() is built from these ideas. Here we write our own. Two modifiers refine inlining: noinline on a lambda parameter keeps that one lambda as a real object (so you can store or forward it), and crossinline forbids non-local returns when a lambda is invoked from another context.
// A practical reified helper: keep only items of a given type.
// Kotlin's standard library has filterIsInstance, built exactly this way.
inline fun <reified T> List<Any>.onlyOfType(): List<T> {
val result = mutableListOf<T>()
for (item in this) {
if (item is T) result.add(item)
}
return result
}
fun main() {
val mixed: List<Any> = listOf(1, "a", 2, "b", 3, "c")
val strings: List<String> = mixed.onlyOfType<String>()
val ints: List<Int> = mixed.onlyOfType<Int>()
println(strings) // -> [a, b, c]
println(ints) // -> [1, 2, 3]
}You rarely need noinline or crossinline day to day — plain inline plus reified covers the vast majority of real code.
Your turn. Fill in the ___, then run and compare.
// 🎯 YOUR TURN — replace the ___ then run main()
// Complete the reified function so it returns the simple class name of T.
inline fun <reified T> typeName(): String {
// T::class.simpleName gives the class name at runtime.
return ___::class.simpleName ?: "Unknown" // 👉 put "T" in the blank
}
fun main() {
println(typeName<String>()) // ✅ Expected: String
println(typeName<Int>()) // ✅ Expected: Int
}Mini-Challenge: Count by Type
Write a reified function that counts how many list items match a given type.
// 🎯 MINI-CHALLENGE: count items of a given type.
// Save as Count.kt and run main().
//
// 1. Write an inline fun <reified T> countOfType(items: List<Any>): Int
// that returns how many items in the list are of type T.
// (Loop and increment a counter when item is T.)
// 2. With the list below, print the count of Strings and the count of Ints.
//
// ✅ Expected output:
// 2
// 3
fun main() {
val data: List<Any> = listOf("x", 1, 2, "y", 3)
// your code here
}Common Errors (and the fix)
- ❌ "Cannot check for instance of erased type: T" — You wrote is T on a plain generic. ✅ Make the function inline and the parameter reified.
- ❌ "Reified type parameter is only allowed for inline functions" — You used reified without inline. ✅ Add the inline modifier.
- ❌ "Illegal usage of inline-parameter" — You tried to store or return an inlined lambda. ✅ Mark that parameter noinline.
- ❌ Inlining a huge function — Marking a large body inline bloats bytecode for no benefit. ✅ Only inline small functions that take lambdas or need reified.
Pro Tips
- 💡 Use the built-in: list.filterIsInstance<String>() already does the reified filtering for you.
- 💡 reified shines in factories: APIs like JSON parsers use inline fun <reified T> parse(...) so callers write parse<User>(json) with no extra class argument.
- 💡 Measure before optimizing: reach for inline for reified or genuine hot paths, not by default.
📋 Quick Reference — inline & reified
| Concept | Kotlin Syntax |
|---|---|
| Inline function | inline fun run(block: () -> Unit) |
| Reified type | inline fun <reified T> f() |
| Runtime type check | if (item is T) ... |
| Get the class | T::class.simpleName |
| Keep lambda as object | noinline block: () -> Unit |
| Forbid non-local return | crossinline block: () -> Unit |
🎉 Lesson Complete!
- ✅ inline copies a function's body into the call site
- ✅ Inlining removes the heap allocation of a passed lambda
- ✅ Higher-order functions benefit most, especially in loops
- ✅ reified keeps a generic type available at runtime
- ✅ noinline and crossinline refine lambda behaviour
Practice quiz
What does marking a function 'inline' do?
- Runs it on a separate thread
- Copies its body into each call site
- Makes it asynchronous
- Caches its return value
Answer: Copies its body into each call site. inline copies the function body — and passed lambdas — directly into the call site.
Which functions benefit most from inlining?
- Functions with no parameters
- Recursive functions
- Higher-order functions that take lambdas
- Functions returning Unit
Answer: Higher-order functions that take lambdas. Inlining removes the per-call lambda object, so higher-order functions benefit most.
What problem does inlining a lambda parameter avoid?
- Heap allocation of a lambda object
- Stack overflow
- Type erasure of return types
- Null pointer exceptions
Answer: Heap allocation of a lambda object. Normally each passed lambda becomes a heap object; inlining removes that allocation.
A 'reified' type parameter is only allowed on which kind of function?
- Suspend functions
- Abstract functions
- Extension functions
- inline functions
Answer: inline functions. reified requires inline, because the type is substituted as the body is inlined.
What does reified let you write that a plain generic cannot?
- item is T and T::class
- a return statement
- a default argument
- a nullable type
Answer: item is T and T::class. With reified, T survives at runtime so you can write is T and T::class.
Why is 'item is T' normally illegal in a generic function?
- T must be a class
- Generic types are erased at runtime on the JVM
- is only works on numbers
- T is always null
Answer: Generic types are erased at runtime on the JVM. JVM type erasure removes T at runtime, so is T won't compile without reified.
What does 'noinline' on a lambda parameter do?
- Forbids returning from the lambda
- Makes the whole function inline
- Keeps that one lambda as a real object so it can be stored or passed on
- Deletes the lambda
Answer: Keeps that one lambda as a real object so it can be stored or passed on. noinline keeps that lambda as a real object so you can store or forward it.
What does 'crossinline' enforce?
- It forbids non-local returns from the lambda
- It forces inlining of all callers
- It makes the lambda nullable
- It reifies the type parameter
Answer: It forbids non-local returns from the lambda. crossinline forbids non-local returns when the lambda is invoked from another context.
Which standard library function is built on reified type filtering?
- mapNotNull
- filterIsInstance
- groupBy
- associateWith
Answer: filterIsInstance. filterIsInstance<T>() uses an inline reified type parameter to filter by type.
What is a downside of marking a large function inline?
- It becomes thread-unsafe
- It can bloat the generated bytecode
- It cannot return a value
- It loses type safety
Answer: It can bloat the generated bytecode. Inlining copies code, so inlining big bodies bloats bytecode for little benefit.
Continue this course
- Previous: Higher-Order Functions & Function Types
- Next: Sequences (Lazy Collections) — Process data lazily for long operation chains and large data sets
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What does the 'inline' keyword actually do?
When a function is marked inline, the compiler does not generate a real function call. Instead it copies the function's body — and the body of any lambda passed to it — directly into the place where you called it. For higher-order functions this is significant: normally each lambda you pass becomes a small object on the heap, but with inlining no such object is created, removing allocation and call overhead from hot loops. The behaviour of your program is unchanged; only the generated bytecode differs.
Why do higher-order functions benefit most from inlining?
Every lambda you pass to an ordinary higher-order function is compiled into an object, and calling it goes through an interface method. In a tight loop that runs millions of times, those allocations and indirect calls add up. Inlining eliminates both: the lambda's code is pasted straight into the loop, so there is no object and no virtual call. That is exactly why Kotlin's standard collection helpers like map, filter, and forEach are declared inline.
What is a reified type parameter and why does it need inline?
On the JVM, generic type information is normally erased at runtime, so inside a generic function you cannot write item is T or T::class — the compiler has lost track of what T is. Marking the type parameter reified (only allowed on inline functions) tells the compiler to substitute the concrete type at each call site as it inlines the body, so T is known at runtime and those checks become legal. It requires inline because the substitution happens precisely during the copy-into-call-site step.
What are noinline and crossinline for?
Both refine how a lambda parameter of an inline function behaves. Mark a lambda parameter noinline when you need to store it or pass it on as a real object — that one lambda then is not inlined while the rest still are. Mark it crossinline when the lambda is invoked from a different execution context (for example inside another object or a nested lambda) and you must forbid non-local returns from it. You only reach for these in specific situations; plain inline covers the common cases.