# Inner Classes
Inner classes are like nested classes, but an object of an inner class maintains a reference to the outer class.
An inner
class has an implicit link to the outer class. In the following example, Hotel
is like Airport
from [Nested Classes](javascript:void(0)), but it uses inner
classes. Note that reception
is part of Hotel
, but callReception()
, which is a member of the nested class Room
, accesses reception
without qualification:
// InnerClasses/Hotel.kt
package innerclasses
import atomictest.eq
class Hotel(private val reception: String) {
open inner class Room(val id: Int = 0) {
// Uses 'reception' from outer class:
fun callReception() =
"Room $id Calling $reception"
}
private inner class Closet : Room()
fun closet(): Room = Closet()
}
fun main() {
val nycHotel = Hotel("311")
// You need an outer object to
// create an instance of the inner class:
val room = nycHotel.Room(319)
room.callReception() eq
"Room 319 Calling 311"
val sfHotel = Hotel("0")
val closet = sfHotel.closet()
closet.callReception() eq "Room 0 Calling 0"
}
Because Closet
inherits the inner class Room
, Closet
must also be an inner
class. Nested classes cannot inherit from inner
classes.
Closet
is private
, so it is only visible within the scope of Hotel
.
An inner
object keeps a reference to its associated outer object. Thus, when creating an inner
object you must first have an outer object. You cannot create a Room
object without a Hotel
object, as you see with nycHotel.Room()
.
inner
data
classes are not allowed.
# Qualified this
One of the benefits of classes is the this
reference. You don’t have to explicitly say “the current object” when you access a property or member function.
With a simple class, the meaning of this
is obvious, but with an inner
class, this
could refer to either the inner
object or an outer object. To resolve this issue, Kotlin provides the qualified this
syntax: this
followed by @
and the name of the target class.
Consider three levels of classes: an outer class Fruit
containing an inner
class Seed
, which itself contains an inner
class DNA
:
// InnerClasses/QualifiedThis.kt
package innerclasses
import atomictest.eq
import typechecking.name
class Fruit { // Implicit label @Fruit
fun changeColor(color: String) =
"Fruit $color"
fun absorbWater(amount: Int) {}
inner class Seed { // Implicit label @Seed
fun changeColor(color: String) =
"Seed $color"
fun germinate() {}
fun whichThis() {
// Defaults to the current class:
this.name eq "Seed"
// To clarify, you can redundantly
// qualify the default this:
this@Seed.name eq "Seed"
// Must explicitly access Fruit:
this@Fruit.name eq "Fruit"
// Cannot access a further-inner class:
// this@DNA.name
}
inner class DNA { // Implicit label @DNA
fun changeColor(color: String) {
// changeColor(color) // Recursive
this@Seed.changeColor(color)
this@Fruit.changeColor(color)
}
fun plant() {
// Call outer-class functions
// Without qualification:
germinate()
absorbWater(10)
}
// Extension function:
fun Int.grow() { // Implicit label @grow
// Default is the Int.grow() receiver:
this.name eq "Int"
// Redundant qualification:
this@grow.name eq "Int"
// You can still access everything:
this@DNA.name eq "DNA"
this@Seed.name eq "Seed"
this@Fruit.name eq "Fruit"
}
// Extension functions on outer classes:
fun Seed.plant() {}
fun Fruit.plant() {}
fun whichThis() {
// Defaults to the current class:
this.name eq "DNA"
// Redundant qualification:
this@DNA.name eq "DNA"
// The others must be explicit:
this@Seed.name eq "Seed"
this@Fruit.name eq "Fruit"
}
}
}
}
// Extension function:
fun Fruit.grow(amount: Int) {
absorbWater(amount)
// Calls Fruit's version of changeColor():
changeColor("Red") eq "Fruit Red"
}
// Inner-class extension function:
fun Fruit.Seed.grow(n: Int) {
germinate()
// Calls Seed's version of changeColor():
changeColor("Green") eq "Seed Green"
}
// Inner-class extension function:
fun Fruit.Seed.DNA.grow(n: Int) = n.grow()
fun main() {
val fruit = Fruit()
fruit.grow(4)
val seed = fruit.Seed()
seed.grow(9)
seed.whichThis()
val dna = seed.DNA()
dna.plant()
dna.grow(5)
dna.whichThis()
dna.changeColor("Purple")
}
Fruit
, Seed
and DNA
all have functions called changeColor()
, but there’s no overriding—this is not an inheritance relationship. Because they have the same name and signature, the only way to distinguish them is with a qualified this
, as you see in DNA
’s changeColor()
. Inside plant()
, functions in either of the two outer classes can be called without qualification if there are no name collisions.
Even though it’s an extension function, grow()
can still access all the objects in the outer class. grow()
can be called anywhere the Fruit.Seed.DNA
implicit receiver is available; for example, inside an extension function for DNA
.
# Inner Class Inheritance
An inner class can inherit another inner class from a different outer class. Here, Yolk
in BigEgg
is derived from Yolk
in Egg
:
// InnerClasses/InnerClassInheritance.kt
package innerclasses
import atomictest.*
open class Egg {
private var yolk = Yolk()
open inner class Yolk {
init { trace("Egg.Yolk()") }
open fun f() { trace("Egg.Yolk.f()") }
}
init { trace("New Egg()") }
fun insertYolk(y: Yolk) { yolk = y }
fun g() { yolk.f() }
}
class BigEgg : Egg() {
inner class Yolk : Egg.Yolk() {
init { trace("BigEgg.Yolk()") }
override fun f() {
trace("BigEgg.Yolk.f()")
}
}
init { insertYolk(Yolk()) }
}
fun main() {
BigEgg().g()
trace eq """
Egg.Yolk()
New Egg()
Egg.Yolk()
BigEgg.Yolk()
BigEgg.Yolk.f()
"""
}
BigEgg.Yolk
explicitly names Egg.Yolk
as its base class, and overrides its f()
member function. The function insertYolk()
allows BigEgg
to upcast one of its own Yolk
objects into the yolk
reference in Egg
, so when g()
calls yolk.f()
, the overridden version of f()
is used. The second call to Egg.Yolk()
is the base-class constructor call of the BigEgg.Yolk
constructor. You can see that the overridden version of f()
is used when g()
is called.
As a review of object construction, study the trace
output until it makes sense.
# Local & Anonymous Inner Classes
Classes defined inside member functions are called local inner classes. These can also be created anonymously, using an object expression, or using a [SAM conversion](javascript:void(0)). In all cases, the inner
keyword is not used, but is implied:
// InnerClasses/LocalInnerClasses.kt
package innerclasses
import atomictest.eq
fun interface Pet {
fun speak(): String
}
object CreatePet {
fun home() = " home!"
fun dog(): Pet {
val say = "Bark"
// Local inner class:
class Dog : Pet {
override fun speak() = say + home()
}
return Dog()
}
fun cat(): Pet {
val emit = "Meow"
// Anonymous inner class:
return object: Pet {
override fun speak() = emit + home()
}
}
fun hamster(): Pet {
val squeak = "Squeak"
// SAM conversion:
return Pet { squeak + home() }
}
}
fun main() {
CreatePet.dog().speak() eq "Bark home!"
CreatePet.cat().speak() eq "Meow home!"
CreatePet.hamster().speak() eq "Squeak home!"
}
A local inner class has access to other elements in the function as well as elements in the outer-class object, thus say
, emit
, squeak
and home()
are available within speak()
.
You can identify an anonymous inner class because it uses an object expression, which you see in cat()
. It returns an object
of a class inherited from Pet
that overrides speak()
. Anonymous inner classes are smaller and more straightforward and do not create a named class that will only be used in one place. Even more compact is a [SAM conversion](javascript:void(0)), as seen in hamster()
.
Because inner classes keep a reference to the outer-class object, local inner classes can access all members of the enclosing class:
// InnerClasses/CounterFactory.kt
package innerclasses
import atomictest.*
fun interface Counter {
fun next(): Int
}
object CounterFactory {
private var count = 0
fun new(name: String): Counter {
// Local inner class:
class Local : Counter {
init { trace("Local()") }
override fun next(): Int {
// Access local identifiers:
trace("$name $count")
return count++
}
}
return Local()
}
fun new2(name: String): Counter {
// Instance of an anonymous inner class:
return object: Counter {
init { trace("Counter()") }
override fun next(): Int {
trace("$name $count")
return count++
}
}
}
fun new3(name: String): Counter {
trace("Counter()")
return Counter { // SAM conversion
trace("$name $count")
count++
}
}
}
fun main() {
fun test(counter: Counter) {
(0..3).forEach { counter.next() }
}
test(CounterFactory.new("Local"))
test(CounterFactory.new2("Anon"))
test(CounterFactory.new3("SAM"))
trace eq """
Local() Local 0 Local 1 Local 2 Local 3
Counter() Anon 4 Anon 5 Anon 6 Anon 7
Counter() SAM 8 SAM 9 SAM 10 SAM 11
"""
}
A Counter
keeps track of a count
and returns the next Int
value. new()
, new2()
and new3()
each create a different implementation of the Counter
interface. new()
returns an instance of a named inner class, new2()
returns an instance of an anonymous inner class, and new3()
uses a [SAM conversion](javascript:void(0)) to create an anonymous object. All the resulting Counter
objects have implicit access to the elements of the outer object, thus they are inner classes and not just nested classes. You can see from the output that count
in CounterFactory
is shared by all Counter
objects.
SAM conversions are limited—for example, they do not support init
clauses.
- -
In Kotlin, files can contain multiple top-level classes and functions. Because of this, there’s rarely a need for local classes, so if you do need them they should be basic and straightforward. For example, it’s reasonable to create a simple data
class that’s only used inside a function. If a local class becomes complex, you should probably take it out of the function and make it a regular class.
Exercises and solutions can be found at www.AtomicKotlin.com.