Nested and Inner Classes

Kotlin lets you define a class inside another class, and the meaning changes depending on one keyword. Write a class inside another class with no extra keyword, and you get a nested class — a self-contained class that happens to live inside another one but has no connection to any particular instance of the outer class. Add the inner keyword, and you get an inner class, which carries an implicit reference to the specific outer instance that created it, so it can read and even mutate that instance’s properties. This single keyword is one of the small but deliberate ways Kotlin departs from Java’s defaults, and getting it right matters both for correctness and for avoiding subtle memory leaks.

Overview / How It Works

In Java, a class declared inside another class is, by default, a non-static inner class: it silently holds a reference back to the enclosing instance (the compiler generates a hidden field, often called this$0). You have to add static to opt out and get a plain nested class with no outer reference. Kotlin flips this default. A class declared inside another class with no modifier behaves like Java’s static nested class: it has no reference to any outer instance and cannot see the outer class’s instance members. To get Java’s inner-class behavior — an implicit reference to the enclosing instance — you must explicitly write inner class.

This flip is deliberate. Holding an unnecessary reference to an outer instance is a classic source of memory leaks (a long-lived collection or callback that stores an inner-class instance can keep the whole outer object alive long after it should have been garbage collected). By making “no outer reference” the default, Kotlin nudges you toward that safer choice unless you specifically ask for the alternative.

Under the hood, the two compile to different JVM bytecode shapes. A plain nested class compiles to a class file like Outer$Nested with an ordinary constructor and no synthetic outer field — you construct it directly, without needing an Outer instance at all: Outer.Nested(). An inner class compiles to Outer$Inner with a synthetic field holding the enclosing instance, and its constructor implicitly takes that outer instance as an extra parameter. That’s why you construct an inner class through an instance of the outer class: outerInstance.Inner(), not Outer.Inner().

Inside an inner class, you can refer to the outer class’s members directly by name, exactly as if they were local. If a property or parameter in the inner class shadows a name from the outer class, Kotlin gives you a way to disambiguate: this@Outer refers explicitly to the enclosing Outer instance, while plain this refers to the inner class instance. This qualified-this syntax generalizes to any number of nesting levels — you always name the class whose instance you want.

A few related but distinct constructs are worth knowing about so you don’t conflate them. A local class is a class declared inside a function body; it can capture variables from the enclosing scope much like a lambda. An anonymous object (an object expression, object : SomeType { ... }) creates a one-off instance of an unnamed type, often used where Java would use an anonymous inner class. Neither is the subject of this lesson, but both sit in the same conceptual neighborhood as nested and inner classes. Most importantly, don’t confuse nested/inner classes with a companion object: a companion object is a single, automatically created singleton tied to the class itself (one shared instance for the whole class), whereas a nested or inner class is an ordinary class template you can instantiate as many times as you like. Finally, interfaces can contain nested classes too — never inner, since an interface has no instance to attach to.

Syntax

class Outer(/* constructor params */) {
    class Nested {
        // does NOT have access to Outer's members
        // instantiate as: Outer.Nested()
    }

    inner class Inner {
        // DOES have access to Outer's members via an implicit outer reference
        // instantiate as: outerInstance.Inner()
    }
}
Form Outer reference? How to instantiate
class Nested { ... } No Outer.Nested()
inner class Inner { ... } Yes (implicit) outerInstance.Inner()

Inside an inner class, use this@Outer to explicitly reach the enclosing instance when a name is shadowed, and plain this for the inner instance itself.

Examples

Example 1: A plain nested class

class Outer(val name: String) {
    class Nested {
        fun greet(): String = "Hello from Nested"
    }
}

fun main() {
    val nested = Outer.Nested()
    println(nested.greet())
}

Output:

Hello from Nested

Notice that Nested is created via Outer.Nested() — no Outer instance is ever created. Nested also has no way to see Outer‘s name property; if you tried to reference name inside Nested, the compiler would reject it as an unresolved reference, because a plain nested class simply doesn’t carry a link to any outer instance.

Example 2: An inner class and qualified this

class Outer(val name: String) {
    val greeting: String = "Hello"

    inner class Inner(val name: String) {
        fun greet(): String {
            return "$greeting, ${this.name} (inner) / ${this@Outer.name} (outer)"
        }
    }
}

fun main() {
    val outer = Outer("Kotlin")
    val inner = outer.Inner("World")
    println(inner.greet())
}

Output:

Hello, World (inner) / Kotlin (outer)

The inner class reads greeting straight from Outer with no qualification, because it has an implicit reference to the outer instance. Its own constructor parameter is also named name, which shadows Outer‘s name property, so this.name resolves to the inner class’s value while this@Outer.name explicitly reaches back to the outer instance’s value.

Example 3: A realistic mix — inner class for state, nested class for a value type

import kotlin.math.abs

class BankAccount(val owner: String, private var balance: Double) {
    private val history = mutableListOf<String>()

    inner class TransactionLogger {
        fun log(amount: Double) {
            val action = if (amount >= 0) "deposited" else "withdrew"
            history.add("$owner: $action ${abs(amount)}, balance now $balance")
        }
    }

    fun deposit(amount: Double) {
        balance += amount
        TransactionLogger().log(amount)
    }

    fun printHistory() {
        history.forEach { println(it) }
    }

    data class Currency(val code: String, val symbol: String)
}

fun main() {
    val account = BankAccount("Alice", 100.0)
    account.deposit(50.0)
    account.deposit(-20.0)
    account.printHistory()

    val usd = BankAccount.Currency("USD", "$")
    println("Currency: ${usd.code} (${usd.symbol})")
}

Output:

Alice: deposited 50.0, balance now 150.0
Alice: withdrew 20.0, balance now 130.0
Currency: USD ($)

This example shows both forms serving different purposes in one class. TransactionLogger is marked inner because it genuinely needs to read owner and the private, mutable balance from the specific account it’s logging for — that’s exactly the case inner exists for. Currency, by contrast, is a plain nested data class: it represents a value (a currency code and symbol) that has nothing to do with any particular account, so it’s created directly via BankAccount.Currency(...) without needing an account instance at all. Being a data class, Currency also gets a generated equals, hashCode, toString, and copy for free, which is why it’s a good fit for a small immutable value type nested for organization.

How It Works Step by Step

  1. BankAccount("Alice", 100.0) constructs an account with balance = 100.0 and an empty, mutable history list. The list reference itself is a val, but its contents can still change — val only prevents reassigning history to a different list, not mutating the list it points to.
  2. account.deposit(50.0) adds 50.0 to balance, making it 150.0, then creates a TransactionLogger(). Because TransactionLogger is an inner class, that construction implicitly carries a reference to account, so log can read owner and the now-updated balance and append a formatted line to history.
  3. account.deposit(-20.0) repeats the process: balance becomes 130.0, and the logger records a “withdrew” line since the amount is negative.
  4. printHistory() iterates the (still same-reference) history list and prints each recorded line in order.
  5. BankAccount.Currency("USD", "$") constructs a Currency value with no BankAccount instance involved at all — proof that the nested (non-inner) class truly stands alone.

Common Mistakes

Mistake 1: Forgetting inner and expecting outer access anyway

class Outer(val name: String) {
    class Nested {
        fun greet(): String = "Hello, $name" // ERROR: unresolved reference 'name'
    }
}

Without inner, Nested has no reference to any Outer instance, so name simply isn’t in scope — this fails to compile. The fix is to add inner, which gives the class its implicit outer reference:

class Outer(val name: String) {
    inner class Nested {
        fun greet(): String = "Hello, $name"
    }
}

fun main() {
    val outer = Outer("Kotlin")
    println(outer.Nested().greet())
}

Output:

Hello, Kotlin

Mistake 2: Instantiating an inner class without an outer instance

class Outer(val label: String) {
    inner class Inner {
        fun show() = println("Label: $label")
    }
}

fun main() {
    val inner = Outer.Inner() // ERROR: an inner class requires an instance of the containing class
    inner.show()
}

Once a class is inner, it can no longer be constructed the “static” way. It always needs a specific outer instance to attach to:

class Outer(val label: String) {
    inner class Inner {
        fun show() = println("Label: $label")
    }
}

fun main() {
    val outer = Outer("Widget")
    val inner = outer.Inner()
    inner.show()
}

Output:

Label: Widget

Mistake 3: Marking a class inner when it doesn’t need outer access

It’s tempting to reach for inner out of habit, especially coming from Java where nested classes are inner by default. But marking a class inner when it never actually reads the outer instance forces every caller to first obtain an outer instance just to build an unrelated helper or value type, and it keeps a hidden reference to that outer instance alive for as long as the inner instance survives. If a nested type doesn’t touch the outer class’s members, leave off inner; that was the case for Currency in Example 3 above.

Best Practices

  • Default to a plain nested class; add inner only when the class genuinely needs to read or mutate the specific outer instance’s members.
  • Use nested classes for small, self-contained helper or value types that logically belong to the outer class (like Node inside a linked list, or Currency inside BankAccount), even though they have nothing to do with any single outer instance.
  • Remember that an inner class holds a strong implicit reference to its outer instance — avoid stashing inner-class instances in long-lived static collections, caches, or callbacks, since doing so can keep the outer instance from being garbage collected.
  • Use this@Outer whenever an inner class parameter or property shadows a name from the outer class, so the code is unambiguous to read.
  • Don’t reach for inner just because that’s Java’s default — in Kotlin it’s an explicit, deliberate opt-in, and the compiler will tell you plainly if you left it off but needed it.
  • Reserve inner/nested classes for a real “belongs to” relationship; if a type is genuinely independent, a top-level class (or a class in its own file) is usually clearer than nesting it purely for source-file organization.

Practice Exercises

  • Define an outer class Library with a mutable list of book titles, and a nested (non-inner) data class Book(val title: String, val author: String). Create two Book instances directly via Library.Book(...) and print them.
  • Define a class Thermostat(var temperature: Int) with an inner class Adjuster that has a method increase(by: Int) which adds to the outer temperature. Create a Thermostat, use an Adjuster to raise the temperature by 5, then print the final temperature. Expected output: 25 if you start at 20.
  • Take the broken snippet from Mistake 1 in this lesson (a nested class trying to read an outer property without inner), and rewrite it correctly two different ways: once by adding inner, and once by instead passing the needed value as a constructor parameter to the nested class. Compare which approach fits better when the nested type shouldn’t be tied to a specific outer instance.

Summary

  • A nested class (class Nested { ... } with no modifier) has no reference to any outer instance and is instantiated as Outer.Nested().
  • An inner class (inner class Inner { ... }) holds an implicit reference to the outer instance that created it, can read and mutate its members, and is instantiated as outerInstance.Inner().
  • Kotlin’s default is the opposite of Java’s: Java’s plain nested class is inner by default; Kotlin’s is not, and you opt in explicitly with inner.
  • this@Outer lets you explicitly reach the enclosing instance when a name is shadowed inside an inner class.
  • Prefer plain nested classes unless outer access is truly needed; unnecessary inner classes can keep an outer instance alive longer than intended.
  • Interfaces can contain nested classes too, but never inner ones, since interfaces have no instances.
  • Don’t confuse nested/inner classes (ordinary class templates, instantiable many times) with a companion object (a single automatic singleton per class).