Question
In Kotlin, I understand that an inline function can sometimes improve performance, but it can also increase generated code size. I am not sure when it is actually appropriate to use one.
For example, Kotlin documentation often shows a pattern like this:
lock(l) {
foo()
}
The idea is that instead of creating a function object for the lambda parameter and then calling it, the compiler could generate code similar to this:
l.lock()
try {
foo()
} finally {
l.unlock()
}
However, when I inspect a non-inline version like this, it seems that Kotlin does not always create a visible function object in the way I expected:
fun lock(lock: Lock, block: () -> Unit) {
lock.lock()
try {
block()
} finally {
lock.unlock()
}
}
Why does it seem like no function object is created for the non-inline case, and when should inline actually be used in Kotlin?
Short Answer
By the end of this page, you will understand what Kotlin inline functions do, why they are mainly useful for higher-order functions, why lambdas in non-inline functions may still appear optimized, and how to decide whether inline is worth using in real code. You will also learn the trade-off between runtime overhead and code size.
Concept
Kotlin inline functions tell the compiler to copy the function body directly into the call site instead of generating a normal function call.
This matters most for higher-order functions: functions that take other functions, usually lambdas, as parameters.
Consider this function:
fun lock(lock: Lock, block: () -> Unit) {
lock.lock()
try {
block()
} finally {
lock.unlock()
}
}
This is a higher-order function because it accepts block, which is a function.
Without inline, the usual model is:
- the lambda may be represented as a function object
- that object is passed to
lock lockcallsblock()
With inline, the compiler can replace the call with the actual code of both the function and the lambda.
inline fun lock(lock: , block: () -> ) {
lock.lock()
{
block()
} {
lock.unlock()
}
}
Mental Model
Think of a normal higher-order function call like hiring a helper.
- You write down instructions in a lambda.
- The program wraps those instructions into a little object.
- The function receives that object and says, “Now execute these instructions.”
An inline function is more like skipping the helper entirely.
- Instead of packaging the instructions,
- the compiler pastes the instructions directly where the function is called.
So:
- non-inline = pass a callable package around
- inline = paste the code in place
If the instructions are tiny and used often, pasting them can be faster. If the instructions are large and used in many places, pasting them everywhere can make the program bigger.
Syntax and Examples
The basic syntax is:
inline fun runAction(action: () -> Unit) {
action()
}
Calling it:
runAction {
println("Hello")
}
A more realistic example is a lock wrapper:
inline fun <T> withLock(lock: Lock, block: () -> T): T {
lock.lock()
return try {
block()
} finally {
lock.unlock()
}
}
Usage:
val result = withLock(myLock) {
"done"
}
Why this is a good inline candidate
- The function body is small.
- It takes a lambda.
- It is a wrapper around common repeated logic.
- It may be called frequently.
Non-inline version
: T {
lock.lock()
{
block()
} {
lock.unlock()
}
}
Step by Step Execution
Consider this inline function:
inline fun withMessage(block: () -> Unit) {
println("Start")
block()
println("End")
}
And this call:
withMessage {
println("Work")
}
Conceptual inline expansion
The compiler can treat it roughly like this:
println("Start")
println("Work")
println("End")
Step by step
- The call to
withMessageis found. - Because the function is marked
inline, the compiler copies its body into the call site. - The lambda body is also copied into the place where
block()appears. - The final code behaves like regular sequential code.
Compare with non-inline
fun withMessageNormal(block: () -> ) {
println()
block()
println()
}
Real World Use Cases
Inline functions are commonly used in situations like these:
Resource handling
inline fun <T> useResource(block: () -> T): T {
println("open")
return try {
block()
} finally {
println("close")
}
}
Useful for wrapping setup and cleanup logic.
Locking and concurrency
inline fun <T> withLock(lock: Lock, block: () -> T): T {
lock.lock()
return try {
block()
} finally {
lock.unlock()
}
}
This keeps critical-section code short and safe.
Collection utilities
Kotlin's standard library uses inline heavily in functions like:
letrunapply
Real Codebase Usage
In real projects, developers use inline selectively, not everywhere.
Common pattern: tiny wrappers around lambdas
inline fun <T> measure(block: () -> T): T {
val start = System.nanoTime()
return try {
block()
} finally {
println(System.nanoTime() - start)
}
}
This is common because the wrapper is small and the lambda is the main work.
Guard-style helper functions
inline fun ifDebug(block: () -> Unit) {
if (BuildConfig.DEBUG) {
block()
}
}
If not in debug mode, the block is skipped. Inlining can reduce wrapper overhead.
Validation and error handling helpers
inline fun requireValid(condition: Boolean, lazyMessage: () -> String) {
if (!condition) IllegalArgumentException(lazyMessage())
}
Common Mistakes
1. Using inline on ordinary functions with no lambdas
inline fun add(a: Int, b: Int): Int {
return a + b
}
This usually brings little value.
Better approach
Just write a normal function unless you have a measured reason.
fun add(a: Int, b: Int): Int = a + b
2. Assuming non-inline always creates a new object every call
Beginners often think this always happens:
- call function
- allocate a brand new lambda object
- invoke it
But the compiler may reuse a singleton for non-capturing lambdas, and the JVM may optimize further.
So the cost is often possible overhead, not always a guaranteed heavy overhead.
3. Inlining large functions
inline fun hugeFunction {
block()
}
Comparisons
| Concept | What it does | Best use case | Trade-off |
|---|---|---|---|
| Normal function | Performs a regular function call | Default choice for most functions | Small call overhead |
inline function | Copies function body into call site | Small higher-order functions with lambdas | Larger generated code |
| Non-inline lambda | Lambda may be represented as an object | General-purpose higher-order code | Possible allocation/call overhead |
| Inline lambda | Lambda body is copied into caller | Performance-sensitive wrappers and control-flow helpers | Can increase bytecode size |
noinline parameter | Prevents a specific lambda from being inlined | When lambda must be stored or passed onward |
Cheat Sheet
Quick rules
- Use
inlinemainly for small higher-order functions. - The biggest benefit is with lambda parameters.
- Do not inline large functions without a reason.
- Do not expect huge gains automatically.
- Non-inline lambdas may still be optimized by the compiler or JVM.
Basic syntax
inline fun doWork(block: () -> Unit) {
block()
}
Good candidates
- lock wrappers
- timing helpers
- validation helpers with lazy messages
- scope-style functions
- tiny DSL/builders
Usually bad candidates
- ordinary arithmetic or utility functions with no lambdas
- large functions called in many places
- code with no measurable overhead issue
Important terms
- higher-order function: a function that takes or returns another function
- lambda: an anonymous function like
{ println("Hi") } - capturing lambda: a lambda that uses values from outer scope
- non-capturing lambda: a lambda that uses no outside variables
Why non-inline may still look cheap
FAQ
Why doesn't a non-inline Kotlin lambda always create a new object?
Because the compiler can optimize non-capturing lambdas by reusing a singleton instance, and the JVM can apply further runtime optimizations.
Should I mark every higher-order function as inline?
No. Use inline when the function is small and the benefit is meaningful. Too much inlining increases code size.
Is inline only about performance?
No. It also enables language features such as non-local returns from lambdas.
Are inline functions always faster?
Not always. They can reduce call overhead, but they also increase bytecode size. The best choice depends on the function's size and usage.
When is inline most useful in Kotlin?
It is most useful for small wrapper functions that take lambdas, such as locking, resource handling, timing, logging, and scope-style utilities.
Why does Kotlin standard library use inline so often?
Because many standard functions are tiny higher-order utilities called frequently, so inlining often improves ergonomics and can reduce overhead.
Can I inline a function without lambda parameters?
Yes, but it is usually not helpful. Kotlin primarily benefits from inline when lambdas or reified type parameters are involved.
What is the simplest rule for beginners?
Default to normal functions. Reach for inline when writing a small higher-order helper and you understand the reason for using it.
Mini Project
Description
Build a small Kotlin utility that safely executes code while holding a lock. This demonstrates when an inline higher-order function is useful: wrapping repeated setup and cleanup logic around a lambda.
Goal
Create a reusable withLock helper and use it to update shared state safely.
Requirements
- Define a
withLockfunction that accepts aLockand a lambda. - Mark the function
inline. - Ensure the lock is always released with
try/finally. - Use the helper to modify shared data.
- Print the final result to verify the code ran correctly.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.
Android AlarmManager Example: Scheduling Tasks with AlarmManager
Learn how to use Android AlarmManager to schedule tasks, set alarms, and handle broadcasts with a simple beginner example.