Kotlin - Inheritance & Polymorphism

Kotlin Inheritance & Polymorphism

Inheritance allows classes to inherit properties and methods from other classes. Learn how to create class hierarchies, override methods, and implement polymorphism in Kotlin.

Basic Inheritance

Key Difference: In Kotlin, classes are final by default. You must explicitly mark classes as `open` to allow inheritance, promoting composition over inheritance.

Open Classes and Inheritance

// Base class must be marked as 'open'
open class Vehicle(val brand: String, val model: String) {
    open val maxSpeed: Int = 100
    
    open fun start() {
        println("$brand $model is starting...")
    }
    
    open fun stop() {
        println("$brand $model is stopping...")
    }
    
    // Final method (cannot be overridden)
    fun getInfo(): String {
        return "$brand $model (Max speed: ${maxSpeed} km/h)"
    }
}

// Derived class
class Car(brand: String, model: String, val doors: Int) : Vehicle(brand, model) {
    override val maxSpeed: Int = 200
    
    override fun start() {
        println("Car engine starting with key...")
        super.start()  // Call parent implementation
    }
    
    // Additional method specific to Car
    fun openTrunk() {
        println("Trunk is now open")
    }
}

// Usage
val car = Car("Toyota", "Camry", 4)
car.start()           // Car engine starting with key... Toyota Camry is starting...
println(car.getInfo()) // Toyota Camry (Max speed: 200 km/h)
car.openTrunk()       // Trunk is now open

Constructor Inheritance

// Base class with primary constructor
open class Animal(val name: String, val species: String) {
    init {
        println("Animal created: $name ($species)")
    }
    
    open fun makeSound() {
        println("$name makes a sound")
    }
}

// Derived class with additional properties
class Dog(name: String, val breed: String) : Animal(name, "Canine") {
    override fun makeSound() {
        println("$name barks: Woof! Woof!")
    }
    
    fun fetch() {
        println("$name is fetching the ball")
    }
}

// Derived class with secondary constructor
class Cat : Animal {
    val isIndoor: Boolean
    
    // Secondary constructor
    constructor(name: String, isIndoor: Boolean) : super(name, "Feline") {
        this.isIndoor = isIndoor
    }
    
    override fun makeSound() {
        println("$name meows: Meow!")
    }
    
    fun climb() {
        val location = if (isIndoor) "cat tree" else "tree"
        println("$name climbs the $location")
    }
}

// Usage
val dog = Dog("Buddy", "Golden Retriever")  // Animal created: Buddy (Canine)
dog.makeSound()  // Buddy barks: Woof! Woof!
dog.fetch()      // Buddy is fetching the ball

val cat = Cat("Whiskers", true)  // Animal created: Whiskers (Feline)
cat.makeSound()  // Whiskers meows: Meow!
cat.climb()      // Whiskers climbs the cat tree

Method Overriding

Override Rules and super Keyword

open class Shape(val color: String) {
    open fun area(): Double = 0.0
    open fun perimeter(): Double = 0.0
    
    open fun describe(): String {
        return "A $color shape with area ${area()} and perimeter ${perimeter()}"
    }
    
    // Final method
    fun getColor(): String = color
}

class Rectangle(color: String, val width: Double, val height: Double) : Shape(color) {
    override fun area(): Double = width * height
    override fun perimeter(): Double = 2 * (width + height)
    
    override fun describe(): String {
        val baseDescription = super.describe()
        return "$baseDescription (Rectangle: ${width}x${height})"
    }
}

class Circle(color: String, val radius: Double) : Shape(color) {
    override fun area(): Double = Math.PI * radius * radius
    override fun perimeter(): Double = 2 * Math.PI * radius
    
    override fun describe(): String {
        return "A $color circle with radius $radius, area %.2f, circumference %.2f"
            .format(area(), perimeter())
    }
}

// Usage
val rectangle = Rectangle("blue", 5.0, 3.0)
val circle = Circle("red", 4.0)

println(rectangle.describe())
// A blue shape with area 15.0 and perimeter 16.0 (Rectangle: 5.0x3.0)

println(circle.describe())
// A red circle with radius 4.0, area 50.27, circumference 25.13

Abstract Classes

Abstract Classes and Methods

// Abstract class cannot be instantiated
abstract class Appliance(val brand: String, val model: String) {
    // Abstract property
    abstract val powerConsumption: Int
    
    // Abstract method
    abstract fun turnOn()
    abstract fun turnOff()
    
    // Concrete method
    fun getSpecs(): String {
        return "$brand $model - Power: ${powerConsumption}W"
    }
    
    // Open method that can be overridden
    open fun performMaintenance() {
        println("Performing basic maintenance on $brand $model")
    }
}

class WashingMachine(brand: String, model: String, val capacity: Int) : Appliance(brand, model) {
    override val powerConsumption: Int = 2000
    private var isRunning = false
    
    override fun turnOn() {
        isRunning = true
        println("Washing machine is now ON - Ready to wash $capacity kg")
    }
    
    override fun turnOff() {
        isRunning = false
        println("Washing machine is now OFF")
    }
    
    override fun performMaintenance() {
        super.performMaintenance()
        println("Cleaning the lint filter and checking water connections")
    }
    
    fun startWashCycle(program: String) {
        if (isRunning) {
            println("Starting $program wash cycle")
        } else {
            println("Please turn on the washing machine first")
        }
    }
}

class Refrigerator(brand: String, model: String, val temperature: Int) : Appliance(brand, model) {
    override val powerConsumption: Int = 150
    private var isOn = false
    
    override fun turnOn() {
        isOn = true
        println("Refrigerator is now ON - Cooling to ${temperature}°C")
    }
    
    override fun turnOff() {
        isOn = false
        println("Refrigerator is now OFF - Warning: Food may spoil!")
    }
    
    fun adjustTemperature(newTemp: Int) {
        if (isOn) {
            println("Adjusting temperature from ${temperature}°C to ${newTemp}°C")
        } else {
            println("Cannot adjust temperature - refrigerator is off")
        }
    }
}

// Usage
val washer = WashingMachine("LG", "TurboWash", 8)
val fridge = Refrigerator("Samsung", "CoolMax", 4)

println(washer.getSpecs())    // LG TurboWash - Power: 2000W
washer.turnOn()               // Washing machine is now ON - Ready to wash 8 kg
washer.startWashCycle("Heavy Duty")
washer.performMaintenance()

println(fridge.getSpecs())    // Samsung CoolMax - Power: 150W
fridge.turnOn()               // Refrigerator is now ON - Cooling to 4°C

Polymorphism

Runtime Polymorphism

// Base class
open class Employee(val name: String, val id: Int, open val baseSalary: Double) {
    open fun calculatePay(): Double = baseSalary
    open fun getRole(): String = "Employee"
    
    open fun work() {
        println("$name is working as ${getRole()}")
    }
}

// Derived classes
class Manager(name: String, id: Int, baseSalary: Double, val teamSize: Int) : Employee(name, id, baseSalary) {
    override fun calculatePay(): Double = baseSalary + (teamSize * 1000)
    override fun getRole(): String = "Manager"
    
    override fun work() {
        super.work()
        println("Managing a team of $teamSize people")
    }
    
    fun conductMeeting() {
        println("$name is conducting a team meeting")
    }
}

class Developer(name: String, id: Int, baseSalary: Double, val programmingLanguage: String) : Employee(name, id, baseSalary) {
    override fun calculatePay(): Double = baseSalary + 5000 // Tech bonus
    override fun getRole(): String = "Developer"
    
    override fun work() {
        super.work()
        println("Coding in $programmingLanguage")
    }
    
    fun writeCode() {
        println("$name is writing $programmingLanguage code")
    }
}

class Intern(name: String, id: Int) : Employee(name, id, 30000.0) {
    override fun getRole(): String = "Intern"
    
    override fun work() {
        super.work()
        println("Learning and assisting with various tasks")
    }
}

// Polymorphic usage
fun processEmployees(employees: List) {
    println("=== Employee Processing ===")
    
    for (employee in employees) {
        println("\n--- ${employee.name} ---")
        println("Role: ${employee.getRole()}")
        println("Salary: $${employee.calculatePay()}")
        employee.work()
        
        // Type checking and casting
        when (employee) {
            is Manager -> {
                employee.conductMeeting()
                println("Team size: ${employee.teamSize}")
            }
            is Developer -> {
                employee.writeCode()
                println("Primary language: ${employee.programmingLanguage}")
            }
            is Intern -> {
                println("This is an intern - providing mentorship")
            }
        }
    }
    
    // Calculate total payroll
    val totalPayroll = employees.sumOf { it.calculatePay() }
    println("\nTotal Payroll: $$totalPayroll")
}

// Usage
val employees = listOf(
    Manager("Alice Johnson", 1, 80000.0, 5),
    Developer("Bob Smith", 2, 75000.0, "Kotlin"),
    Developer("Carol Brown", 3, 70000.0, "Python"),
    Intern("David Wilson", 4)
)

processEmployees(employees)

Polymorphism with Collections

// Shape hierarchy for polymorphic collections
abstract class DrawableShape(val name: String) {
    abstract fun draw()
    abstract fun calculateArea(): Double
    
    fun printInfo() {
        println("$name - Area: ${calculateArea()}")
    }
}

class PolygonShape(name: String, private val sides: List>) : DrawableShape(name) {
    override fun draw() {
        println("Drawing $name with ${sides.size} vertices")
        sides.forEachIndexed { index, (x, y) ->
            println("  Vertex ${index + 1}: ($x, $y)")
        }
    }
    
    override fun calculateArea(): Double {
        // Simplified area calculation (shoelace formula)
        return sides.size.toDouble() * 10.0  // Placeholder
    }
}

class CircleShape(name: String, private val radius: Double) : DrawableShape(name) {
    override fun draw() {
        println("Drawing $name with radius $radius")
    }
    
    override fun calculateArea(): Double = Math.PI * radius * radius
}

// Function that works with any DrawableShape
fun renderShapes(shapes: List) {
    println("=== Rendering Shapes ===")
    
    shapes.forEach { shape ->
        shape.printInfo()
        shape.draw()
        println()
    }
    
    // Group by type
    val shapesByType = shapes.groupBy { it::class.simpleName }
    println("Shape counts:")
    shapesByType.forEach { (type, shapeList) ->
        println("$type: ${shapeList.size}")
    }
    
    // Total area
    val totalArea = shapes.sumOf { it.calculateArea() }
    println("Total area: %.2f".format(totalArea))
}

// Usage
val shapes = listOf(
    CircleShape("Circle 1", 5.0),
    PolygonShape("Triangle", listOf(0.0 to 0.0, 3.0 to 0.0, 1.5 to 3.0)),
    CircleShape("Circle 2", 3.0),
    PolygonShape("Square", listOf(0.0 to 0.0, 2.0 to 0.0, 2.0 to 2.0, 0.0 to 2.0))
)

renderShapes(shapes)

Advanced Inheritance Patterns

Template Method Pattern

// Template method pattern using inheritance
abstract class DataProcessor {
    // Template method - defines the algorithm structure
    fun processData(data: List): List {
        println("Starting data processing...")
        
        val validated = validateData(data)
        val preprocessed = preprocessData(validated)
        val processed = performProcessing(preprocessed)
        val postprocessed = postprocessData(processed)
        
        println("Data processing completed")
        return postprocessed
    }
    
    // Abstract methods to be implemented by subclasses
    protected abstract fun validateData(data: List): List
    protected abstract fun performProcessing(data: List): List
    
    // Hook methods with default implementations
    protected open fun preprocessData(data: List): List {
        println("Default preprocessing")
        return data
    }
    
    protected open fun postprocessData(data: List): List {
        println("Default postprocessing")
        return data
    }
}

class NumberProcessor : DataProcessor() {
    override fun validateData(data: List): List {
        println("Validating numbers - removing negatives")
        return data.filter { it >= 0 }
    }
    
    override fun performProcessing(data: List): List {
        println("Processing numbers - squaring each value")
        return data.map { it * it }
    }
    
    override fun postprocessData(data: List): List {
        println("Post-processing - sorting results")
        return data.sorted()
    }
}

class StringProcessor : DataProcessor() {
    override fun validateData(data: List): List {
        println("Validating strings - removing empty strings")
        return data.filter { it.isNotBlank() }
    }
    
    override fun performProcessing(data: List): List {
        println("Processing strings - converting to uppercase")
        return data.map { it.uppercase() }
    }
}

// Usage
val numberProcessor = NumberProcessor()
val numbers = listOf(1, -2, 3, 4, -5, 6)
val processedNumbers = numberProcessor.processData(numbers)
println("Result: $processedNumbers")

println()

val stringProcessor = StringProcessor()
val strings = listOf("hello", "", "world", "kotlin", " ")
val processedStrings = stringProcessor.processData(strings)
println("Result: $processedStrings")

Sealed Class Hierarchies

// Sealed classes for restricted inheritance
sealed class Result
data class Success(val data: T) : Result()
data class Error(val message: String, val cause: Throwable? = null) : Result()
object Loading : Result()

// Sealed class for UI states
sealed class UiState {
    object Idle : UiState()
    object Loading : UiState()
    data class Content(val items: List) : UiState()
    data class Error(val message: String) : UiState()
}

// Functions using sealed classes (exhaustive when)
fun  handleResult(result: Result): String {
    return when (result) {
        is Success -> "Got data: ${result.data}"
        is Error -> "Error: ${result.message}"
        is Loading -> "Loading..."
        // No else needed - when is exhaustive
    }
}

fun updateUI(state: UiState) {
    when (state) {
        is UiState.Idle -> println("UI: Ready for user input")
        is UiState.Loading -> println("UI: Showing loading spinner")
        is UiState.Content -> {
            println("UI: Displaying ${state.items.size} items")
            state.items.forEach { println("  - $it") }
        }
        is UiState.Error -> println("UI: Showing error - ${state.message}")
    }
}

// Usage
val results = listOf(
    Success("Hello World"),
    Error("Network timeout"),
    Loading,
    Success(listOf(1, 2, 3, 4, 5))
)

results.forEach { result ->
    println(handleResult(result))
}

val uiStates = listOf(
    UiState.Idle,
    UiState.Loading,
    UiState.Content(listOf("Item 1", "Item 2", "Item 3")),
    UiState.Error("Failed to load data")
)

uiStates.forEach { state ->
    updateUI(state)
    println()
}

Best Practices

✅ Good Practices

  • Prefer composition over inheritance when possible
  • Use sealed classes for restricted hierarchies
  • Mark classes as open only when inheritance is intended
  • Override methods thoughtfully and call super when appropriate
  • Use abstract classes for shared implementation, interfaces for contracts
  • Keep inheritance hierarchies shallow (prefer 2-3 levels max)

❌ Avoid

  • Deep inheritance hierarchies (hard to maintain)
  • Inheriting just to reuse code (use composition instead)
  • Making everything open (violates encapsulation)
  • Complex inheritance hierarchies where simple interfaces would work
  • Overriding methods without understanding the contract
Architecture Note: Kotlin's approach to inheritance (final by default, explicit open) encourages better design. Consider if you need inheritance or if composition with interfaces would be more flexible and maintainable.

Practice Exercises

  1. Create a vehicle hierarchy with different types of vehicles
  2. Implement a game character system with different character classes
  3. Build an employee payroll system using inheritance and polymorphism
  4. Create a shape drawing application with polymorphic rendering
  5. Design a file system hierarchy (files, directories, links)

Quick Quiz

  1. Why are Kotlin classes final by default?
  2. What's the difference between open and abstract classes?
  3. How do you call a parent class method from an overridden method?
  4. What makes sealed classes special?
Show answers
  1. To prevent unintended inheritance and encourage composition over inheritance
  2. Open classes can be instantiated and inherited; abstract classes cannot be instantiated but can be inherited
  3. Use the `super` keyword: `super.methodName()`
  4. Sealed classes restrict inheritance to a fixed set of subclasses, enabling exhaustive when expressions