What object does a Kotlin lambda actually become at runtime, and what are the performance implications of passing functions as values?
answer
- lambda -> FunctionN object with invoke
- capturing = allocates; non-capturing = singleton
- generics box primitives at invoke boundary
- inline copies bodies -> no object, no call
- noinline / crossinline tune inlining
basics
~10 sA lambda becomes an object implementing a FunctionN interface (like Function1) with an invoke method. Creating these objects and capturing variables can add allocations, which inline functions can remove.
solid answer
~40 sA non-inlined lambda compiles to a class implementing the matching `FunctionN` interface — `Function0`, `Function1<P,R>`, etc. — whose `invoke` holds the body. A capturing lambda (one that uses outside variables) becomes a class with fields for the captured state, allocated each time it's created; a non-capturing lambda can be cached as a singleton by the compiler. Passing such a value through a higher-order function means an object allocation plus a virtual `invoke` call, and primitives get boxed at the generic boundary. Kotlin's `inline` keyword fixes this: the compiler copies the higher-order function's body and the lambda's body into the call site, eliminating the FunctionN object and the call entirely. That's why stdlib hot-path HOFs (`map`, `filter`, `forEach`, `let`, `run`) are inline. `noinline` and `crossinline` tune which lambdas get inlined.
code
kotlin · 10 linesinline fun <T> retry(times: Int, block: () -> T): T {
var last: Throwable? = null
repeat(times) {
try { return block() } // non-local return, allowed because inline
catch (e: Throwable) { last = e }
}
throw last!!
}
// After inlining, no Function0 object is allocated for `block`.
val value = retry(3) { computeSomething() }go deeper
Knows a lambda is some object you can call; not expected to name FunctionN.
Names FunctionN/invoke and knows inline reduces overhead.
Explains capturing vs singleton, boxing, virtual invoke cost, and how inline/noinline/crossinline change codegen.
Reasons about bytecode size vs allocation tradeoffs, JIT call-site behavior, and when designing an inline HOF API is justified.
## What a lambda becomes At the source level a lambda is a value; at runtime it's an **object**. The compiler generates a class implementing the appropriate **`FunctionN` interface**: - `Function0<R>` for `() -> R` - `Function1<P1, R>` for `(P1) -> R` - … up to `Function22`, plus `FunctionN` for higher arities. Each has a single method: `invoke`. Calling `f(x)` dispatches to `f.invoke(x)`. ## Capturing vs non-capturing ```kotlin val k = 10 val cap: (Int) -> Int = { it + k } // captures k -> object holds a field, allocated per creation val pure: (Int) -> Int = { it + 1 } // captures nothing -> compiler may reuse a singleton ``` - A **capturing** lambda becomes a class with fields for the captured variables; a new instance is allocated whenever the lambda is created. - A **non-capturing** lambda has no state, so the compiler can emit one shared instance (a singleton). ## Costs of functions-as-values 1. **Allocation** of the FunctionN object (for capturing lambdas). 2. **Virtual call** through `invoke` (megamorphic call sites hurt JIT). 3. **Boxing**: `Function1<Int, Int>` is generic, so primitive `Int` arguments/returns are boxed to `Integer` at the boundary. In tight loops these add up. ## How `inline` removes the cost ```kotlin inline fun <T> measure(block: () -> T): T { /* ... */ return block() } ``` Mark the HOF `inline` and the compiler **copies** both the function's body and each lambda's body into the call site. Result: **no FunctionN object, no invoke call, no boxing**, and lambdas can perform **non-local returns** and use **reified** type parameters. This is exactly why stdlib hot-path functions (`map`, `filter`, `forEach`, `let`, `also`, `run`, `with`) are `inline`. ### Tuning inlining - `noinline` on a parameter: keep that lambda as a real FunctionN object (e.g., to store it). - `crossinline`: allow inlining but forbid non-local returns (needed when the lambda is invoked from another execution context). ## Practical guidance Don't manually fear lambdas — the stdlib and `inline` make most of them free. Reach for `inline` when you write your own hot-path HOF; profile before micro-optimizing elsewhere.
- Why are stdlib functions like map and forEach declared inline?To eliminate the per-element FunctionN allocation and virtual invoke call, and to allow non-local returns, making these hot-path operations as cheap as a hand-written loop.
- When would you mark a lambda parameter noinline?When you need to store the lambda, pass it to a non-inline function, or otherwise treat it as a real object instead of inlining it.
A non-inlined lambda is a parcel you ship (allocate + deliver). Inlining is teleporting the contents straight into the destination — no parcel, no shipping cost.
saying these in an interview costs you the question
- Claiming every lambda always allocates (non-capturing ones can be singletons)
- Saying inline functions still create FunctionN objects
- Ignoring boxing of primitives across the generic invoke boundary
- Thinking inline is free with no downsides (it grows bytecode)
- Confusing crossinline (no non-local return) with noinline (not inlined)