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
- Create a vehicle hierarchy with different types of vehicles
- Implement a game character system with different character classes
- Build an employee payroll system using inheritance and polymorphism
- Create a shape drawing application with polymorphic rendering
- Design a file system hierarchy (files, directories, links)
Quick Quiz
- Why are Kotlin classes final by default?
- What's the difference between open and abstract classes?
- How do you call a parent class method from an overridden method?
- What makes sealed classes special?
Show answers
- To prevent unintended inheritance and encourage composition over inheritance
- Open classes can be instantiated and inherited; abstract classes cannot be instantiated but can be inherited
- Use the `super` keyword: `super.methodName()`
- Sealed classes restrict inheritance to a fixed set of subclasses, enabling exhaustive when expressions