skip to content

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.

level: seniorimportance: should knowfreq 30%

answer

  1. Kotlin allows mutating captured var (Java requires effectively final)
  2. compiler boxes it in Ref.IntRef/ObjectRef with `element`
  3. same holder shared -> writes visible both ways
  4. deferred call sees latest value, not capture-time
  5. val capture is not boxed; Ref is not thread-safe

basics

~20 s

Kotlin 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 s

Unlike 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 lines
kotlin
fun 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

for a junior

Knows a local function can read outer variables; may not know var mutation specifics.

for a middle

Knows Kotlin allows mutating captured vars, unlike Java's effectively-final rule.

for a senior

Explains Ref boxing, shared-holder semantics, and the deferred-execution/loop-capture pitfall.

for a principal

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

context