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
BankAccount("Alice", 100.0)constructs an account withbalance = 100.0and an empty, mutablehistorylist. The list reference itself is aval, but its contents can still change —valonly prevents reassigninghistoryto a different list, not mutating the list it points to.account.deposit(50.0)adds 50.0 tobalance, making it 150.0, then creates aTransactionLogger(). BecauseTransactionLoggeris an inner class, that construction implicitly carries a reference toaccount, sologcan readownerand the now-updatedbalanceand append a formatted line tohistory.account.deposit(-20.0)repeats the process:balancebecomes 130.0, and the logger records a “withdrew” line since the amount is negative.printHistory()iterates the (still same-reference)historylist and prints each recorded line in order.BankAccount.Currency("USD", "$")constructs aCurrencyvalue with noBankAccountinstance 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
inneronly 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
Nodeinside a linked list, orCurrencyinsideBankAccount), 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@Outerwhenever an inner class parameter or property shadows a name from the outer class, so the code is unambiguous to read. - Don’t reach for
innerjust 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
Librarywith a mutable list of book titles, and a nested (non-inner)data class Book(val title: String, val author: String). Create twoBookinstances directly viaLibrary.Book(...)and print them. - Define a class
Thermostat(var temperature: Int)with aninner class Adjusterthat has a methodincrease(by: Int)which adds to the outertemperature. Create aThermostat, use anAdjusterto raise the temperature by 5, then print the finaltemperature. Expected output:25if 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 addinginner, 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 asOuter.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 asouterInstance.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@Outerlets 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
innerclasses can keep an outer instance alive longer than intended. - Interfaces can contain nested classes too, but never
innerones, 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).
