# Nested Classes

Nested classes enable more refined structures within your objects.

A nested class is simply a class within the namespace of the outer class. The implication is that the outer class “owns” the nested class. This feature is not essential, but nesting a class can clarify your code. Here, Plane is nested within Airport:

// NestedClasses/Airport.kt
package nestedclasses
import atomictest.eq
import nestedclasses.Airport.Plane

class Airport(private val code: String) {
  open class Plane {
    // Can access private properties:
    fun contact(airport: Airport) =
      "Contacting ${airport.code}"
  private class PrivatePlane : Plane()
  fun privatePlane(): Plane = PrivatePlane()

fun main() {
  val denver = Airport("DEN")
  var plane = Plane()                   // [1]
  plane.contact(denver) eq "Contacting DEN"
  // Can't do this:
  // val privatePlane = Airport.PrivatePlane()
  val frankfurt = Airport("FRA")
  plane = frankfurt.privatePlane()
  // Can't do this:
  // val p = plane as PrivatePlane      // [2]
  plane.contact(frankfurt) eq "Contacting FRA"

In contact(), the nested class Plane has access to the private property code in the airport argument, whereas an ordinary class would not have this access. Other than that, Plane is simply a class inside the Airport namespace.

Creating a Plane object does not require an Airport object, but if you create it outside the Airport class body, you must ordinarily qualify the constructor call in [1]. By importing nestedclasses.Airport.Plane we avoid this qualification.

A nested class can be private, as with PrivatePlane. Making it private means that PrivatePlane is completely invisible outside the body of Airport, so you cannot call the PrivatePlane constructor outside of Airport. If you define and return a PrivatePlane from a member function, as seen in privatePlane(), the result must be upcast to a public type (assuming it extends a public type), and cannot be downcast to the private type, as seen in [2].

Here’s an example of nesting where Cleanable is a base class for both the enclosing class House and all the nested classes. clean() goes through a List of parts and calls clean() for each one, producing a kind of recursion:

// NestedClasses/NestedHouse.kt
package nestedclasses
import atomictest.*

abstract class Cleanable(val id: String) {
  open val parts: List<Cleanable> = listOf()
  fun clean(): String {
    val text = "$id clean"
    if (parts.isEmpty()) return text
    return "${parts.joinToString(
      " ", "(", ")",
      transform = Cleanable::clean)} $text\n"

class House : Cleanable("House") {
  override val parts = listOf(
    Bedroom("Master Bedroom"),
    Bedroom("Guest Bedroom")
  class Bedroom(id: String) : Cleanable(id) {
    override val parts =
      listOf(Closet(), Bathroom())
    class Closet : Cleanable("Closet") {
      override val parts =
        listOf(Shelf(), Shelf())
      class Shelf : Cleanable("Shelf")
    class Bathroom : Cleanable("Bathroom") {
      override val parts =
        listOf(Toilet(), Sink())
      class Toilet : Cleanable("Toilet")
      class Sink : Cleanable("Sink")

fun main() {
  House().clean() eq """
  (((Shelf clean Shelf clean) Closet clean
   (Toilet clean Sink clean) Bathroom clean
  ) Master Bedroom clean
   ((Shelf clean Shelf clean) Closet clean
   (Toilet clean Sink clean) Bathroom clean
  ) Guest Bedroom clean
  ) House clean

Notice the multiple levels of nesting. For example, Bedroom contains Bathroom which contains Toilet and Sink.

# Local Classes

Classes that are nested inside functions are called local classes:

// NestedClasses/LocalClasses.kt
package nestedclasses

fun localClasses() {
  open class Amphibian
  class Frog : Amphibian()
  val amphibian: Amphibian = Frog()

Amphibian looks like a candidate to be an interface rather than an open class. However, local interfaces are not allowed.

Local open classes should be rare; if you need one, what you’re trying to make is probably significant enough to create a regular class.

Amphibian and Frog are invisible outside localClasses(), so you can’t return them from the function. To return objects of local classes, you must upcast them to a class or interface defined outside the function:

// NestedClasses/ReturnLocal.kt
package nestedclasses

interface Amphibian

fun createAmphibian(): Amphibian {
  class Frog : Amphibian
  return Frog()

fun main() {
  val amphibian = createAmphibian()
  // amphibian as Frog

Frog is still invisible outside createAmphibian()—in main(), you cannot cast amphibian to a Frog because Frog isn’t available, so Kotlin reports the attempt to use Frog as an “unresolved reference.”

# Classes Inside Interfaces

Classes can be nested within interfaces:

// NestedClasses/WithinInterface.kt
package nestedclasses
import atomictest.eq

interface Item {
  val type: Type
  data class Type(val type: String)

class Bolt(type: String) : Item {
  override val type = Item.Type(type)

fun main() {
  val items = listOf(
    Bolt("Slotted"), Bolt("Hex")
  items.map(Item::type) eq
    "[Type(type=Slotted), Type(type=Hex)]"

In Bolt, the val type must be overridden and assigned using the qualified class name Item.Type.

# Nested Enumerations

Enumerations are classes, so they can be nested inside other classes:

// NestedClasses/Ticket.kt
package nestedclasses
import atomictest.eq
import nestedclasses.Ticket.Seat.*

class Ticket(
  val name: String,
  val seat: Seat = Coach
) {
  enum class Seat {
  fun upgrade(): Ticket {
    val newSeat = values()[
      (seat.ordinal + 1)
    return Ticket(name, newSeat)
  fun meal() = when(seat) {
    Coach -> "Bag Meal"
    Premium -> "Bag Meal with Cookie"
    Business -> "Hot Meal"
    First -> "Private Chef"
  override fun toString() = "$seat"

fun main() {
  val tickets = listOf(
    Ticket("Summer", Premium),
    Ticket("Squanchy", Business),
    Ticket("Beth", First)
  tickets.map(Ticket::meal) eq
    "[Bag Meal, Bag Meal with Cookie, " +
    "Hot Meal, Private Chef]"
  tickets.map(Ticket::upgrade) eq
    "[Premium, Business, First, First]"
  tickets eq
    "[Coach, Premium, Business, First]"
  tickets.map(Ticket::meal) eq
    "[Bag Meal, Bag Meal with Cookie, " +
    "Hot Meal, Private Chef]"

upgrade() adds one to the ordinal value of the seat, then uses the library function coerceAtMost() to ensure the new value does not exceed First.ordinal before indexing into values() to produce the new Seat type. Following functional programming principles, upgrading a Ticket produces a new Ticket rather than modifying the old one.

meal() uses when to test every type of Seat and this suggests we could use polymorphism instead.

Enumerations cannot be nested within functions, and cannot inherit from other classes (including other enumerations).

Interfaces can contain nested enumerations. FillIt is a game-like simulation that fills a square grid with randomly-chosen X and O marks:

// NestedClasses/FillIt.kt
package nestedclasses
import nestedclasses.Game.State.*
import nestedclasses.Game.Mark.*
import kotlin.random.Random
import atomictest.*

interface Game {
  enum class State { Playing, Finished }
  enum class Mark { Blank, X ,O }

class FillIt(
  val side: Int = 3, randomSeed: Int = 0
): Game {
  val rand = Random(randomSeed)
  private var state = Playing
  private val grid =
    MutableList(side * side) { Blank }
  private var player = X
  fun turn() {
    val blanks = grid.withIndex()
      .filter { it.value == Blank }
    if(blanks.isEmpty()) {
      state = Finished
    } else {
      grid[blanks.random(rand).index] = player
      player = if (player == X) O else X
  fun play() {
    while(state != Finished)
  override fun toString() =

fun main() {
  val game = FillIt(8, 17)
  game eq """
  [O, X, O, X, O, X, X, X]
  [X, O, O, O, O, O, X, X]
  [O, O, X, O, O, O, X, X]
  [X, O, O, O, O, O, X, O]
  [X, X, O, O, X, X, X, O]
  [X, X, O, O, X, X, O, X]
  [O, X, X, O, O, O, X, O]
  [X, O, X, X, X, O, X, X]

For testability, we seed a Random object with randomSeed to produce identical output each time the program runs. Each element of grid is initialized with Blank. In turn(), we first find all cells containing Blank, along with their indices. If there are no more Blank cells then the simulation is complete. Otherwise, we use random() with our seeded generator to select one of the Blank cells. Because we used withIndex() earlier, we must select the index property to produce the location of the cell we want to change.

To display the List in the form of a two-dimensional grid, toString() uses the chunked() library function to break the List into pieces, each of length side, then joins these together with newlines.

Try experimenting with FillIt using different sides and randomSeeds.

Exercises and solutions can be found at www.AtomicKotlin.com.