Kotlin - Nested & Inner Classes

Overview

Kotlin supports nested classes, inner classes, and anonymous classes to help organize code and create focused, encapsulated components. This tutorial covers the differences between these class types, their use cases, and practical implementation patterns.

🎯 Learning Objectives:
  • Understand nested classes and their access rules
  • Learn about inner classes and outer class references
  • Master anonymous classes and object expressions
  • Apply these concepts in real-world scenarios
  • Choose the right class type for different situations

Nested Classes

A nested class is declared inside another class but doesn't hold a reference to the outer class instance. Nested classes are like static nested classes in Java.

Basic Nested Class

class Outer {
    private val outerProperty = "Outer"
    
    class Nested {
        fun nestedFunction() = "Hello from nested class"
        // Cannot access outerProperty directly
    }
}

fun main() {
    val nested = Outer.Nested()
    println(nested.nestedFunction()) // Hello from nested class
}
Key Point: Nested classes cannot access private members of the outer class because they don't hold a reference to the outer instance.

Nested Classes with Companion Objects

class Calculator {
    companion object {
        const val PI = 3.14159
        
        class MathConstants {
            const val E = 2.71828
            const val GOLDEN_RATIO = 1.618
        }
    }
}

fun main() {
    println(Calculator.PI)
    println(Calculator.MathConstants.E)
}

Inner Classes

Inner classes are marked with the inner keyword and hold a reference to the outer class instance, allowing access to outer class members.

Basic Inner Class

class Outer {
    private val outerProperty = "Outer Property"
    
    inner class Inner {
        fun accessOuter() = "Accessing: $outerProperty"
        
        fun getOuterReference(): Outer = this@Outer
    }
}

fun main() {
    val outer = Outer()
    val inner = outer.Inner() // Note: need outer instance
    println(inner.accessOuter()) // Accessing: Outer Property
}

Practical Example: LinkedList Node

class LinkedList {
    private var head: Node? = null
    
    inner class Node(val data: T) {
        var next: Node? = null
        
        fun addAfter(data: T): Node {
            val newNode = Node(data) // Can create Node directly
            newNode.next = this.next
            this.next = newNode
            return newNode
        }
        
        fun remove() {
            // Access outer class to modify head if needed
            if (this == head) {
                head = this.next
            }
        }
    }
    
    fun add(data: T): Node {
        val newNode = Node(data)
        newNode.next = head
        head = newNode
        return newNode
    }
}

Anonymous Classes

Anonymous classes are created using object expressions, implementing interfaces or extending classes on-the-fly.

Anonymous Interface Implementation

interface EventListener {
    fun onEvent(message: String)
}

class EventEmitter {
    private val listeners = mutableListOf()
    
    fun addListener(listener: EventListener) {
        listeners.add(listener)
    }
    
    fun emit(message: String) {
        listeners.forEach { it.onEvent(message) }
    }
}

fun main() {
    val emitter = EventEmitter()
    
    // Anonymous class implementing EventListener
    emitter.addListener(object : EventListener {
        override fun onEvent(message: String) {
            println("Received: $message")
        }
    })
    
    emitter.emit("Hello World")
}

Anonymous Class Extending Abstract Class

abstract class Animal {
    abstract fun makeSound()
    fun sleep() = println("Sleeping...")
}

fun createAnimal(): Animal {
    return object : Animal() {
        override fun makeSound() = println("Some animal sound")
    }
}

fun main() {
    val animal = createAnimal()
    animal.makeSound()
    animal.sleep()
}

Anonymous Objects with Properties

fun createTemporaryObject() = object {
    val name = "Temporary"
    val id = 12345
    fun display() = println("$name: $id")
}

fun main() {
    val temp = createTemporaryObject()
    temp.display() // Temporary: 12345
    println("Name: ${temp.name}") // Name: Temporary
}

Local Classes

Classes can be declared inside functions (local classes). They have access to local variables and outer class members.

class Outer {
    private val outerVal = "outer"
    
    fun createLocalClass(localParam: String) {
        val localVal = "local"
        
        class LocalClass {
            fun display() {
                println("Outer: $outerVal")
                println("Local: $localVal")
                println("Param: $localParam")
            }
        }
        
        val local = LocalClass()
        local.display()
    }
}

fun main() {
    Outer().createLocalClass("parameter")
    // Output:
    // Outer: outer
    // Local: local
    // Param: parameter
}

Real-World Examples

Builder Pattern with Nested Classes

class HttpRequest private constructor(
    val url: String,
    val method: String,
    val headers: Map,
    val body: String?
) {
    class Builder {
        private var url: String = ""
        private var method: String = "GET"
        private var headers: MutableMap = mutableMapOf()
        private var body: String? = null
        
        fun url(url: String) = apply { this.url = url }
        fun method(method: String) = apply { this.method = method }
        fun header(key: String, value: String) = apply { headers[key] = value }
        fun body(body: String) = apply { this.body = body }
        
        fun build() = HttpRequest(url, method, headers.toMap(), body)
    }
}

fun main() {
    val request = HttpRequest.Builder()
        .url("https://api.example.com/users")
        .method("POST")
        .header("Content-Type", "application/json")
        .body("""{"name": "John", "age": 30}""")
        .build()
}

Event System with Inner Classes

class EventSystem {
    private val events = mutableMapOf>()
    
    inner class EventHandler(
        private val eventType: String,
        private val callback: (String) -> Unit
    ) {
        fun handle(data: String) = callback(data)
        
        fun unregister() {
            events[eventType]?.remove(this)
        }
    }
    
    fun on(eventType: String, callback: (String) -> Unit): EventHandler {
        val handler = EventHandler(eventType, callback)
        events.getOrPut(eventType) { mutableListOf() }.add(handler)
        return handler
    }
    
    fun emit(eventType: String, data: String) {
        events[eventType]?.forEach { it.handle(data) }
    }
}

fun main() {
    val eventSystem = EventSystem()
    
    val handler = eventSystem.on("message") { data ->
        println("Received message: $data")
    }
    
    eventSystem.emit("message", "Hello!")
    handler.unregister()
}

Best Practices

When to Use Each Type

  • Nested Class: When you need a helper class that doesn't require access to outer instance
  • Inner Class: When the class is tightly coupled to the outer class and needs access to its members
  • Anonymous Class: For one-time implementations of interfaces or abstract classes
  • Local Class: When you need a class only within a specific function scope

Performance Considerations

class PerformanceExample {
    // Nested class - no outer reference (memory efficient)
    class NestedHelper {
        fun help() = "Help"
    }
    
    // Inner class - holds outer reference (uses more memory)
    inner class InnerHelper {
        fun help() = "Help from inner"
    }
}

// Prefer nested when outer access isn't needed
class PreferNested {
    class Utility {  // No 'inner' needed
        fun utility() = "utility function"
    }
}

Common Patterns

State Machine with Nested Classes

class StateMachine {
    private var currentState: State = IdleState()
    
    abstract class State {
        abstract fun handle(input: String): State
        abstract fun getName(): String
    }
    
    class IdleState : State() {
        override fun handle(input: String): State {
            return when (input) {
                "start" -> ProcessingState()
                else -> this
            }
        }
        override fun getName() = "Idle"
    }
    
    class ProcessingState : State() {
        override fun handle(input: String): State {
            return when (input) {
                "complete" -> CompleteState()
                "error" -> ErrorState()
                else -> this
            }
        }
        override fun getName() = "Processing"
    }
    
    class CompleteState : State() {
        override fun handle(input: String) = IdleState()
        override fun getName() = "Complete"
    }
    
    class ErrorState : State() {
        override fun handle(input: String) = IdleState()
        override fun getName() = "Error"
    }
    
    fun processInput(input: String) {
        println("Current state: ${currentState.getName()}")
        currentState = currentState.handle(input)
        println("New state: ${currentState.getName()}")
    }
}

Key Takeaways

  • Nested classes don't hold outer class references and cannot access private outer members
  • Inner classes hold outer references and can access all outer class members
  • Anonymous classes are perfect for one-time interface implementations
  • Local classes have access to local variables and outer class members
  • Choose the appropriate type based on coupling and access requirements
  • Consider memory implications when choosing between nested and inner classes

Practice Exercises

  1. Create a `BinaryTree` class with an inner `Node` class that can access tree methods
  2. Implement a nested `Builder` class for a `User` data class
  3. Use anonymous classes to create different sorting strategies for a list
  4. Build a simple calculator with nested operation classes

Quiz

  1. What's the main difference between nested and inner classes?
  2. Can a nested class access private members of its outer class?
  3. When would you prefer an anonymous class over a named class?
Show Answers
  1. Nested classes don't hold a reference to the outer instance; inner classes do and can access outer members.
  2. No, nested classes cannot access private members of the outer class because they don't have an outer instance reference.
  3. Use anonymous classes for one-time implementations, event handlers, or when you need a quick implementation without creating a separate class file.