skip to content

What does the compiler actually generate for nested vs inner classes, and how should that inform your default choice when designing an API?

level: principalimportance: nice to knowfreq 20%

answer

  1. Nested -> JVM static class
  2. Inner -> synthetic this$0 outer field
  3. Outer passed as hidden constructor arg
  4. Default nested, escalate to inner
  5. Inner couples lifetime to outer instance

basics

~20 s

A nested class compiles to a standalone (static-like) class with no link to the outer. An inner class compiles to a class with a hidden field pointing at the outer object. Default to nested and only add inner when you truly need that link.

solid answer

~40 s

On the JVM, a Kotlin nested class compiles to a `static` nested class with no synthetic outer field — it stands alone. An `inner` class compiles to a non-static nested class carrying a synthetic field (conventionally `this$0`) that holds the enclosing instance and is passed to its constructor; that is what powers `this@Outer`. Design-wise this means: (1) nested is cheaper (no extra field, no instance coupling) and safer (no implicit retention); (2) inner couples each instance's lifetime to an outer instance, which is exactly what you want for iterators/builders/views but a liability for callbacks. The principled default is **nested**, escalating to `inner` only when the type's contract genuinely requires live outer state. For helpers that need only a few fields, prefer nested + explicit parameters so retention is visible at the call site.

code

kotlin · 13 lines
kotlin
class Registry<T> {
    private val data = mutableListOf<T>()
    fun add(x: T) { data += x }
    // inner: needs live outer state + T -> justified
    inner class Cursor : Iterator<T> {
        private var i = 0
        override fun hasNext() = i < data.size
        override fun next() = data[i++]
    }
    // nested: pure helper, no outer state -> default
    data class Stats(val size: Int)
    fun stats() = Stats(data.size)
}

go deeper

for a junior

Knows inner keeps an outer reference and nested does not, even if unsure of bytecode names.

for a middle

Can state that inner stores the outer in a hidden field and nested compiles to a static-like class.

for a senior

Explains the synthetic this$0 constructor injection and applies a nested-by-default rule with concrete exceptions.

for a principal

Frames the choice as an API-design and lifetime-ownership decision, balancing cost, retention, ergonomics, and generics across a codebase, and codifies a team default.

## What the compiler emits - **Nested** (`class Outer { class N }`): a JVM `static` nested class. No synthetic field, no outer parameter in its constructor. Behaves like any top-level class that merely lives in the `Outer` namespace. - **Inner** (`inner class I`): a JVM non-static nested class. The compiler adds a **synthetic field** (the historical name is `this$0`) initialized from a hidden constructor parameter that receives the enclosing instance. `this@Outer` reads that field. This is why you must construct it from an instance (`outer.I()`): the outer reference is a constructor argument under the hood. ```kotlin class Outer { inner class I // ~ class I(private val this$0: Outer) class N // ~ static class N } ``` ## Design implications 1. **Cost & coupling**: inner adds one reference field per instance and binds its lifetime to an outer instance. Nested has neither. 2. **Retention/leaks**: the synthetic outer field is a strong reference — see the leak discussion. Nested cannot leak the outer because there is nothing to leak. 3. **Construction ergonomics**: inner requires an outer instance; nested is constructible standalone, which is friendlier for factories and tests. 4. **Generics/state access**: only inner sees the outer's instance state and per-instance type parameters. ## A practical decision rule - Default to **nested**. - Use **inner** only when the type's job is to operate on a specific outer instance's live state (custom `Iterator`, `ListIterator`, builder/DSL receiver, a mutable view). - If a nested helper needs a little outer data, pass it explicitly (a snapshot, an id) so retention is visible and bounded. - Reserve `companion object` for the single per-class singleton (factories, constants), not for per-instance helpers. ## Key takeaways - Nested → static class, no outer field; inner → synthetic `this$0` outer field. - Inner trades a reference + lifetime coupling for live outer access. - Principled default: nested; escalate to inner only on genuine need.

  • What is the synthetic field that backs `this@Outer` historically called on the JVM?
    `this$0` — a synthetic field holding the enclosing instance, populated from a hidden constructor parameter.
  • Give one case where escalating from nested to inner is clearly justified.
    A custom `Iterator`/`ListIterator` or mutable view that must read the outer instance's live backing data and its type parameter `T`.

saying these in an interview costs you the question

  • Claiming nested classes also get a synthetic outer field
  • Saying inner and nested compile to identical bytecode
  • Defaulting to `inner` without a state/lifetime justification
  • Thinking the outer reference is set via reflection rather than a constructor arg
  • Treating `companion object` as a substitute for a per-instance inner helper

context