Fundamentals 11 min read

Master Kotlin’s inline, crossinline, and noinline: Boost Performance and Control Flow

Explore how Kotlin’s inline, crossinline, and noinline modifiers transform lambda handling, eliminate object creation overhead, enable non‑local returns, and control function expansion, with detailed code examples comparing Java anonymous classes, Kotlin lambdas, bytecode, and practical usage scenarios.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Master Kotlin’s inline, crossinline, and noinline: Boost Performance and Control Flow

1. Java and Anonymous Inner Classes

In Java, using higher‑order functions starts with defining an interface, such as OnClickListener in Android:

public interface OnClickListener {
    void onClick(View v);
}

When using it, you create an anonymous inner class implementing the interface:

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println("Hello World");
    }
});

From Java 8 onward, functional interfaces can be replaced with a lambda expression:

view.setOnClickListener(v -> System.out.println("Hello World"));

2. Kotlin and Higher‑Order Functions

Kotlin natively supports lambdas and higher‑order functions, allowing you to pass a function type directly as a parameter without defining an interface first:

fun wrapper(block: () -> Unit) {
    block()
}

Calling it looks like:

wrapper {
    println("Inside Block")
}

Viewing the generated bytecode reveals that the lambda is compiled into an instance of Function0 and invoked via invoke(). Kotlin provides predefined function interfaces Function0, Function1, Function2, … up to Function22 in Functions.kt:

// Functions.kt
public interface Function0<out R> : Function<R> {
    public operator fun invoke(): R
}
public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}
... // up to Function22
Small question: Functions.kt only defines up to Function22 . What about functions with more than 22 parameters?

Thus, Kotlin’s lambda handling is conceptually similar to Java’s anonymous classes, but the compiler can inline the code to avoid creating function objects.

3. The inline Modifier: Eliminating Object and Call Overhead

Marking a function with inline tells the compiler to replace the call site with the function’s body and its lambda arguments, removing the overhead of function calls and object creation.

inline fun wrapper(block: () -> Unit) {
    println("Start")
    block()
    println("End")
}

Calling it:

wrapper {
    println("Inside Block")
}

The compiled code becomes roughly:

println("Start")
println("Inside Block")
println("End")
Enable the “Inline” option in the Kotlin Bytecode viewer to see the difference.

4. Non‑Local Returns with inline

If a lambda passed to an inline function contains a return, that return jumps out of the enclosing function (a non‑local return):

inline fun wrapper(block: () -> Unit) {
    println("Start")
    block()
    println("End")
}

fun test() {
    wrapper {
        println("Inside")
        return
    }
    println("Outside")
}

Running test() prints only:

Start
Inside

The return exits test() because the lambda is inlined.

5. crossinline : Preventing Non‑Local Returns

When a lambda is intended to run in another thread or coroutine, a non‑local return would be illegal. Adding the crossinline modifier forces the compiler to reject such returns:

inline fun runAsync(crossinline block: () -> Unit) {
    thread {
        block()
    }
}

Attempting to write return inside the lambda produces a compilation error, requiring either a local return ( return@runAsync) or the use of crossinline to block the non‑local return.

6. noinline : Excluding a Lambda from Inlining

Sometimes you need to keep a lambda as a first‑class object (e.g., store it in a variable). Mark the parameter with noinline to prevent inlining:

inline fun wrapper(noinline block: () -> Unit) {
    val b = block // OK, block is not inlined
    b()
}

You can mix inlined and non‑inlined parameters:

inline fun run(block1: () -> Unit, noinline block2: () -> Unit) {
    val task = block2 // compiled
    task()
    block1()
}

7. Summary of Modifier Behaviors

The following points capture the essential differences: inline – expands the function, allows non‑local returns, cannot store the lambda as a variable. crossinline – expands the function but forbids non‑local returns. noinline – does not expand the function, allows the lambda to be stored or passed around.

Kotlin’s lambda objects incur some performance cost; inline removes that cost, but when you need to keep the lambda as an object, noinline resolves the conflict.

8. Thought Exercise: Why Does Kotlin Need Non‑Local Returns?

Without non‑local returns, code would become more verbose and control flow harder to read, especially in DSLs, composable APIs, and coroutines. For example, an inline function can early‑exit the caller:

inline fun doIf(condition: Boolean, block: () -> Unit) {
    if (condition) block()
}

fun login(user: String?) {
    doIf(user == null) {
        println("no user")
        return // non‑local return exits login()
    }
    println("login user: $user")
}

If non‑local returns were disallowed, you would need extra flags or manual checks to achieve the same behavior.

LambdaKotlinHigher-Order FunctionscrossinlineInlinenoinline
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.