Skip to content
davthecoder
Understanding inline, crossinline, noinline, reified, and where Keywords in Kotlin
tech

Understanding inline, crossinline, noinline, reified, and where Keywords in Kotlin

Kotlin Android Inline Functions Generics Performance Mobile Development

A practical guide to Kotlin’s inline, crossinline, noinline, reified, and where keywords, with code examples showing when each one matters and how they fit together.

Kotlin keywords inline, crossinline, noinline, reified, and where on a deep-dive cover

The Problem These Keywords Solve

Kotlin makes higher-order functions and generics feel effortless. You pass lambdas around like values and write generic code without much ceremony. That convenience hides a cost. Every lambda you pass is, by default, an object the runtime has to allocate. Every generic type parameter is erased at compile time, so the type you wrote disappears before the program runs.

These five keywords exist to give you control back. inline, noinline, and crossinline decide how lambdas get compiled. reified brings erased types back to life. where lets a single type parameter satisfy several constraints at once. Plenty of developers reach for them on instinct, then hit a compiler error they can’t explain.

Key Takeaways

  • inline copies a function’s body and its lambdas straight into the call site, removing lambda object allocation and enabling non-local returns.
  • noinline opts a single lambda parameter out of inlining so you can store it or pass it onward.
  • crossinline keeps a lambda inlined but forbids non-local returns when the lambda runs in a different execution context.
  • reified only works inside inline functions and lets you access a generic type at runtime, defeating type erasure.
  • where is unrelated to inlining; it declares multiple upper bounds on a generic type parameter.

What Does inline Actually Do?

The inline modifier tells the compiler to copy the function’s body, and the bodies of any lambdas you pass to it, directly into the call site. There is no function call and no lambda object at runtime. The bytecode reads as if you wrote the logic inline by hand.

Heads up: inline is overloaded. This post covers inline functions. Kotlin reuses the word for two other features. Inline (value) classes wrap a single value with no runtime allocation (@JvmInline value class UserId(val raw: String)), and inline properties inline their accessor code. Same keyword, different mechanics. Everything below is about functions.

This matters most for higher-order functions. Without inline, a function that takes a lambda allocates a Function object every time you call it. In a hot loop or a frequently called helper, those allocations add up. The official docs are blunt about the cost: each function is an object that captures a closure, and “memory allocations (both for function objects and classes) and virtual calls introduce runtime overhead” (Kotlin docs, Inline functions).

inline fun measure(block: () -> Unit) {
    val start = System.nanoTime()
    block()
    println("Took ${System.nanoTime() - start} ns")
}

fun main() {
    measure {
        // This lambda body is copied into main().
        // No Function0 object is allocated.
        (1..1_000).sum()
    }
}

Inlining also enables non-local returns. Because the lambda body is pasted into the caller, a plain return inside the lambda returns from the enclosing function, not just the lambda. That is exactly why forEach from the standard library can do this:

fun findFirstEven(numbers: List<Int>): Int? {
    numbers.forEach {
        if (it % 2 == 0) return it // returns from findFirstEven, not just the lambda
    }
    return null
}

A word of caution from experience: inline is not free. Copying a large function body into many call sites bloats your bytecode. The Kotlin team gives the same warning, noting that inlining “may cause the generated code to grow” and advising you to avoid inlining large functions (Kotlin docs, Inline functions). Reserve it for small functions that take lambdas. Inlining a big function with no lambda parameters usually does more harm than good, and the compiler will warn you about it.

When Should You Use noinline?

Once a function is marked inline, all of its lambda parameters are inlined by default. Sometimes that is a problem. An inlined lambda has no object representation, so you cannot store it in a variable, put it in a collection, or pass it to a function that expects a real lambda object. The noinline modifier opts a single parameter out of inlining. As the docs put it, “noinline lambdas… can be manipulated in any way you like, including being stored in fields or passed around” (Kotlin docs, Inline functions).

inline fun runTasks(
    immediate: () -> Unit,
    noinline deferred: () -> Unit
) {
    immediate()                 // inlined, no allocation
    pendingCallbacks.add(deferred) // needs a real object reference, so noinline
}

val pendingCallbacks = mutableListOf<() -> Unit>()

Here immediate stays inlined for performance, while deferred becomes an ordinary lambda object you can stash in a list and invoke later. If you tried to add immediate to the list, the compiler would reject it because there is no object to add.

Use noinline when one lambda needs to outlive the function call or travel somewhere the inlined version cannot go.

What Problem Does crossinline Solve?

crossinline keeps a lambda inlined but bans non-local returns from it. You need this when the lambda is not called directly by the inline function, but instead runs inside another execution context, such as a nested lambda, an anonymous object, or a Runnable. The docs describe exactly this case: when an inline function calls a lambda “not directly from the function body, but from another execution context, such as a local object or a nested function,” non-local control flow is no longer allowed (Kotlin docs, Inline functions).

A non-local return only makes sense when the lambda body executes in the caller’s frame. The moment you hand the lambda to a separate object that runs later, returning from the outer function is impossible. The compiler forces you to acknowledge that with crossinline.

inline fun runOnThread(crossinline block: () -> Unit) {
    val task = Runnable {
        block() // called from inside the Runnable, a different context
    }
    Thread(task).start()
}

Without crossinline, this does not compile. The compiler refuses to inline block inside the Runnable because a non-local return there would have nowhere to return to. Marking the parameter crossinline says “inline it, but I promise not to return non-locally,” and the code compiles.

So the choice is simple. If you call the lambda directly, plain inline is enough. If you wrap it in another object or lambda, you need crossinline. If you need to store or forward it, you need noinline. A single lambda cannot be both noinline and crossinline, since they pull in opposite directions. Both modifiers are also exclusive to inline functions: noinline and crossinline only mean something on a lambda parameter of an inline function, and the compiler rejects them anywhere else.

How Does reified Beat Type Erasure?

On the JVM, generic type arguments are erased at compile time. The Kotlin docs state that “there is no general way to check whether an instance of a generic type was created with certain type arguments at runtime,” so the compiler prohibits checks like list is T (Kotlin docs, Generics: type erasure). The usual workaround is to pass a Class<T> parameter by hand, which is noisy.

reified removes that noise, but it only works inside inline functions. Because the function is inlined, the concrete type is known at every call site, so the compiler can substitute the real type wherever you reference T.

inline fun <reified T> Any.isInstanceOf(): Boolean = this is T

inline fun <reified T> parseJson(json: String): T =
    Gson().fromJson(json, T::class.java)

// Call site: no need to pass User::class anymore.
val user: User = parseJson(jsonString)

The parseJson example is the classic win. Compare it to the non-reified version, where you would write fun <T> parseJson(json: String, clazz: Class<T>) and pass User::class.java at every call. reified lets the call site stay clean while the type information survives into the function body. This is why reified and inline are inseparable: the docs confirm that “normal functions (not marked as inline) cannot have reified type parameters” (Kotlin docs, Reified type parameters). Drop the inline, and reified stops compiling.

reified has limits worth knowing. It hands you is T, as T, and T::class, but not a constructor. You cannot write T() to instantiate the type, and you cannot reach T’s companion object or static members. When you need those, pass a factory lambda or a KClass<T> argument explicitly. You will hit this exact pattern in larger projects, such as the generic repositories in the Kotlin Multiplatform clean architecture blueprint.

What Is the where Keyword For?

where is the odd one out here. It has nothing to do with inlining or lambdas. It declares multiple upper bounds on a generic type parameter. Kotlin lets you write a single bound inline, as in <T : Comparable<T>>, but the moment a type parameter needs two or more bounds, you must move them to a where clause. The rule is explicit in the docs: “only one upper bound can be specified inside the angle brackets. If the same type parameter needs more than one upper bound, you need a separate where-clause” (Kotlin docs, Generics: upper bounds).

Before the constraint, recall how a generic function is declared: the type parameter sits before the function name, and you either pass it explicitly or let the compiler infer it (Kotlin docs, Generic functions).

// The type parameter <T> goes before the function name.
fun <T> singletonList(item: T): List<T> = listOf(item)

val a = singletonList<Int>(1) // explicit type argument
val b = singletonList(1)      // inferred, same result

A single upper bound goes right after the type parameter, inside the angle brackets:

// T is restricted to types that are Comparable with themselves.
fun <T : Comparable<T>> greater(a: T, b: T): T = if (a > b) a else b

That is the limit: one bound in the brackets. The moment a type parameter needs two or more bounds, you move them into a where clause.

// T must be BOTH Comparable<T> and CharSequence at the same time.
fun <T> printSortedLengths(items: List<T>)
    where T : Comparable<T>, T : CharSequence {
    items.sorted().forEach { item -> println(item.length) }
}

Here T must be Comparable<T> so the list can be sorted, and CharSequence so each item exposes .length. Neither bound alone is enough; the where clause expresses the intersection.

where even combines with the keywords above. A reified inline function can carry a where clause when its type argument also needs bounds:

// reified lets us filter by T at runtime; where bounds T to Comparable.
inline fun <reified T> smallestOfType(items: List<Any>): T?
    where T : Comparable<T> {
    return items.filterIsInstance<T>().minOrNull()
}

val mixed: List<Any> = listOf("skip", 3, 1, 2, "x")
println(smallestOfType<Int>(mixed)) // 1

That single declaration pulls together three of the five keywords: inline makes it inlinable, reified keeps T available for filterIsInstance, and where constrains T so minOrNull can compare elements.

Quick Comparison

KeywordApplies toWhat it doesRequires inline?
inlinefunctionCopies body and lambdas to the call site; removes lambda allocation; enables non-local returnsn/a
noinlinelambda parameterOpts one lambda out of inlining so it can be stored or passed onwardYes
crossinlinelambda parameterKeeps the lambda inlined but forbids non-local returnsYes
reifiedtype parameterMakes a generic type available at runtime, defeating type erasureYes
wheretype parameterDeclares multiple upper bounds on a genericNo

When to Use Each: A Decision Guide

Reach for inline when you write a small higher-order function that runs often and you want to remove lambda allocation overhead, or when you need non-local returns. Skip it for large functions without lambda parameters.

Add noinline to a specific parameter the moment you need to store that lambda, add it to a collection, or pass it to a non-inline function. Add crossinline instead when the lambda is invoked from a nested context like a Runnable or anonymous object and you want to keep it inlined.

Reach for reified whenever you find yourself passing a Class<T> argument purely to work around type erasure, and you are willing to make the function inline. And where only earns its place when one type parameter needs more than one upper bound.

A common pitfall worth flagging: developers mark a function inline reflexively, thinking it always means faster. It does not. Inlining a function with no lambda parameters rarely helps and can bloat your output. Let the use case, lambdas, reified types, or non-local returns, justify the keyword. These distinctions surface often in technical screens, too; the Kotlin interview questions drill more of the same fundamentals.

Frequently Asked Questions

Does inline always make code faster?

No. inline removes lambda object allocation and call overhead, which helps for small higher-order functions called frequently. For large functions or functions without lambda parameters, inlining copies the whole body into every call site and bloats the bytecode, often making things worse. Kotlin warns you when inlining is unlikely to help.

Can you use reified without inline?

No. reified depends on inlining. The compiler can only resolve the concrete type because the function body is copied into each call site, where the real type is known. Remove inline and a reified type parameter will not compile.

What is the difference between noinline and crossinline?

noinline turns a lambda back into a real object that is not inlined at all, so you can store or forward it. crossinline still inlines the lambda but forbids non-local returns, which you need when the lambda runs inside another context like a Runnable. They are mutually exclusive on the same parameter.

No. where is purely a generics feature for declaring multiple upper bounds on a type parameter. It is grouped with the others because reified connects generics and inlining, and where is what you use when a generic constraint needs more than one bound. They can appear together in a reified function.

Can a single lambda parameter be both noinline and crossinline?

No. The two modifiers contradict each other. noinline says do not inline this lambda, while crossinline says inline it but restrict its returns. You apply at most one of them to any given lambda parameter.

Conclusion

These five keywords look intimidating as a group, but each answers one clear question. inline controls whether a function and its lambdas are copied to the call site. noinline and crossinline fine-tune individual lambda parameters, one to escape inlining, the other to restrict returns while staying inlined. reified rescues generic types from erasure, and only inside inline functions. where simply layers extra bounds onto a generic.

Learn them by the problem they solve rather than by memorizing definitions, and the compiler errors stop being mysterious. After that, you reach for each one on purpose instead of by trial and error.

If you want to go deeper on Kotlin internals, see the guide to Job types in Kotlin Coroutines for another lifecycle-level concept worth mastering.

Sources

Comments

Loading comments…