skip to content

How does a Kotlin object expression capture variables from its enclosing scope, and how does this differ from Java anonymous classes?

level: middleimportance: should knowfreq 55%

answer

  1. Closure: captures locals/params/this
  2. Kotlin allows capturing & mutating var (Java needs final)
  3. Mutable capture = boxed Ref holder, shared
  4. this = object; this@Outer = enclosing
  5. Implicit outer ref -> leak risk

basics

~20 s

An anonymous object can read and use variables defined around it, like a local variable in the enclosing function. Unlike Java, those variables don't have to be final, and Kotlin even lets the object change them.

solid answer

~50 s

An object expression closes over (captures) variables from its enclosing function or class — local `val`s, `var`s, and `this`. The compiler keeps them alive by referencing them from the generated anonymous class. The big difference from Java is that captured variables need not be `final`: a captured `var` can be both read and modified by the anonymous object. The compiler implements this by boxing the mutable variable into a generated `Ref` holder shared between the enclosing scope and the object, so writes are visible on both sides. This is the same closure mechanism Kotlin uses for lambdas. Capturing a non-static enclosing instance also captures an implicit reference to it, which matters for memory leaks (e.g. holding an Activity on Android). Inside the object, `this` refers to the anonymous object; use a qualified `this@Outer` to reach the enclosing instance.

code

kotlin · 12 lines
kotlin
fun observe(start: Int): Pair<() -> Int, () -> Unit> {
    var value = start                 // captured, mutable
    val read = object : () -> Int { override fun invoke() = value }
    val bump = object : () -> Unit { override fun invoke() { value++ } }
    return read to bump
}

fun main() {
    val (read, bump) = observe(10)
    bump(); bump()
    println(read()) // 12 -> both objects share the same Ref holder
}

go deeper

for a junior

Knows the object can use variables from the surrounding function.

for a middle

Explains closure capture, that var capture is allowed and mutable, and the this@Outer qualification.

for a senior

Describes the Ref-boxing mechanism for shared mutable capture and the implicit outer-instance reference.

for a principal

Reasons about lifecycle/leak implications (e.g. Android), and chooses capture strategy or weak references to avoid retaining large graphs.

## Capturing (closures) When an object expression references a name declared **outside** it — a local variable, a parameter, or a member of the enclosing class — it **captures** that name. The generated anonymous class keeps a reference so the value stays usable even after the enclosing function returns. This is the same **closure** mechanism as Kotlin lambdas. ## Difference from Java - In **Java**, an anonymous class may only capture **effectively final** local variables; it cannot reassign them. - In **Kotlin**, captured locals can be **`var`** and can be **modified** from inside the object. ### How mutation works For a captured **mutable** local, the compiler **boxes** it into a generated holder (e.g. `Ref.IntRef` / `Ref.ObjectRef`). Both the enclosing scope and the anonymous object hold the **same holder**, so a write on either side is visible to the other. ```kotlin fun counter(): () -> Int { var count = 0 // captured as a Ref holder val incrementer = object : () -> Int { override fun invoke(): Int = ++count // mutates the shared holder } return incrementer } ``` ## `this` resolution Inside the object, an unqualified `this` is the **anonymous object itself**. To reach the surrounding instance use a **qualified this**: `this@Outer`. ```kotlin class Screen { val title = "Home" fun makeRenderer() = object : Runnable { override fun run() { println(this) // the anonymous object println([email protected]) // the enclosing Screen } } } ``` ## Memory-leak caution Capturing a member of a non-static enclosing instance captures an **implicit reference to that instance**. On platforms like Android, an anonymous object that outlives its `Activity`/`Fragment` (e.g. stored in a long-lived registry) keeps it alive and **leaks** it. Mitigate by capturing only what you need, or using a top-level/`object` declaration plus weak references. ## Summary - Captures locals, params, and enclosing `this`. - `var` capture allowed and mutable (boxed Ref). - Unqualified `this` = the object; `this@Outer` = the enclosing scope. - Watch for implicit outer-instance references and leaks.

  • Why can Kotlin modify a captured local but Java cannot?
    Kotlin boxes the mutable local into a shared `Ref` holder referenced by both the closure and the enclosing scope; Java captures the value directly and so requires it to be effectively final.
  • How do you reference the enclosing class instance inside the anonymous object?
    Use a qualified `this@OuterClassName`; an unqualified `this` refers to the anonymous object itself.

The anonymous object packs a backpack with references to the variables it needs and carries them wherever it goes.

saying these in an interview costs you the question

  • Saying captured variables must be final in Kotlin
  • Thinking each object gets its own copy of a captured var (it's shared via Ref)
  • Believing unqualified this refers to the enclosing class
  • Ignoring the implicit outer-instance reference / leak risk
  • Claiming object expressions cannot form closures

context