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

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)

Pro Tips

📋 Quick Reference — inline & reified

ConceptKotlin Syntax
Inline functioninline fun run(block: () -> Unit)
Reified typeinline fun <reified T> f()
Runtime type checkif (item is T) ...
Get the classT::class.simpleName
Keep lambda as objectnoinline block: () -> Unit
Forbid non-local returncrossinline block: () -> Unit

🎉 Lesson Complete!

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

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.