A local function captures and mutates a `var` from its enclosing function. Explain what the compiler does under the hood and what subtle behavior this can cause.
answer
- Kotlin allows mutating captured var (Java requires effectively final)
- compiler boxes it in Ref.IntRef/ObjectRef with `element`
- same holder shared -> writes visible both ways
- deferred call sees latest value, not capture-time
- val capture is not boxed; Ref is not thread-safe
basics
~20 sKotlin lets a local function change an outer var. Behind the scenes the variable is boxed in a small holder object so both the outer code and the inner function see the same value. Changes made inside are visible outside.
solid answer
~40 sUnlike Java, where a captured local must be effectively final, Kotlin allows a local function (or lambda) to mutate a captured `var`. The compiler implements this by boxing the variable into a generated reference holder (e.g. `Ref.IntRef`/`Ref.ObjectRef`) on the heap; both the enclosing scope and the local function read and write the holder's `element` field, so mutations are shared in both directions. Subtleties: (1) if the local function is stored and run later or on another thread, it observes whatever the boxed value is at execution time, not at capture time — a classic surprise when capturing a loop variable; (2) the boxing adds a small heap allocation and indirection; (3) capturing a `val` doesn't box (the value is copied/referenced directly). This is the same mechanism that powers closures over `var` in lambdas.
code
kotlin · 9 linesfun runningTotal(values: List<Int>): List<Int> {
var sum = 0 // boxed into Ref.IntRef
val out = mutableListOf<Int>()
fun step(v: Int) { sum += v; out += sum } // mutates captured sum
values.forEach { step(it) }
return out
}
fun main() = println(runningTotal(listOf(1, 2, 3))) // [1, 3, 6]go deeper
Knows a local function can read outer variables; may not know var mutation specifics.
Knows Kotlin allows mutating captured vars, unlike Java's effectively-final rule.
Explains Ref boxing, shared-holder semantics, and the deferred-execution/loop-capture pitfall.
Reasons about allocation cost, thread-safety, and API design to avoid surprising shared mutable capture.
## The capability In Kotlin a local function can **mutate** a `var` it captures from the enclosing scope: ```kotlin fun countMatches(items: List<String>, predicate: (String) -> Boolean): Int { var count = 0 fun tally(item: String) { if (predicate(item)) count++ // mutates enclosing var } items.forEach { tally(it) } return count } ``` This differs from Java, where captured locals must be **effectively final** (you cannot reassign them after capture). ## How the compiler implements it To share mutations across scope boundaries, the compiler **boxes** the captured `var` into a generated reference holder from `kotlin.jvm.internal.Ref`: - `Ref.IntRef`, `Ref.LongRef`, ... for primitives, - `Ref.ObjectRef<T>` for references. The holder has a single mutable field, conventionally `element`. Every read of `count` compiles to `count.element`, every write to `count.element = ...`. Because both the outer code and the local function reference the **same holder object on the heap**, writes from either side are visible to the other. A captured **`val`** does **not** need a `Ref` box — its value can't change, so the compiler can capture it directly without the holder. ## Subtle behaviors 1. **Deferred execution sees the latest value.** If the local function (or a lambda built around it) is stored and invoked later, it reads the holder's current value at call time, not the value at capture time: ```kotlin fun makeCounters(): List<() -> Int> { val fns = mutableListOf<() -> Int>() var i = 0 while (i < 3) { fun read() = i // all closures share the SAME i fns += ::read i++ } return fns } // every closure returns 3, because they share the boxed i ``` To capture a snapshot per iteration, copy into a fresh `val` inside the loop body. 2. **Cost.** Boxing adds a heap allocation and a level of indirection. It's negligible in most code but worth knowing in hot paths. 3. **Concurrency.** The `Ref` holder is **not** synchronized. If the local function is run on another thread, mutating a captured `var` is a data race; use proper synchronization, atomics, or immutable handoff. ## Practical guidance - Capturing `val`s is cheaper and safer; prefer them when you don't need mutation. - Beware loop-variable capture with deferred execution. - Don't rely on captured-`var` mutation across threads without synchronization.
- How does this differ from Java's closure capture?Java requires captured locals to be effectively final (no reassignment). Kotlin permits reassignment by boxing the variable in a Ref holder.
- Is mutating a captured var from another thread safe?No. The Ref holder isn't synchronized, so cross-thread mutation is a data race; use atomics or synchronization.
saying these in an interview costs you the question
- Saying Kotlin captures by value so outer code can't see inner mutations
- Claiming the behavior is identical to Java (effectively final)
- Thinking a captured loop variable snapshots its value per iteration
- Assuming captured-var mutation is thread-safe
- Believing capturing a `val` also requires a Ref box