Skip to content

Kotlin Inheritance — Complete Guide with Examples

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Kotlin Inheritance. We cover key concepts, practical examples, and best practices to help you master this topic.

Kotlin inheritance requires explicit open keyword on classes and methods, uses override for method implementation, supports abstract classes and interfaces, and provides sealed classes for restricted hierarchies.

What You'll Learn

  • Declare open classes and methods for inheritance
  • Override methods and properties with the override keyword
  • Call superclass implementations using super
  • Create abstract classes and methods
  • Use sealed classes for restricted class hierarchies
  • Understand polymorphism and dynamic dispatch
  • Follow best practices for class design

Why It Matters

Inheritance enables code reuse and polymorphism, two pillars of object-oriented programming. Kotlin's approach is intentional: classes are closed by default (like Java 17's sealed by default), preventing accidental inheritance. The open keyword makes the design decision explicit. Abstract classes define partial implementations. Sealed classes model restricted unions like states in a UI component. Understanding these mechanisms lets you design hierarchy structures that are maintainable and safe.

Real-World Use

DodaTech uses sealed classes to represent API response states: Loading, Success, Error, and Empty. This eliminates invalid state combinations and forces exhaustive handling in when expressions. Abstract base classes define common behavior for multiple platform-specific implementations of file parsers.

Learning Path

flowchart LR
  A[Classes] --> B[Inheritance\nYou are here]
  B --> C[Interfaces]
  style B fill:#f90,color:#fff

Open Classes and Methods

Classes and methods are final by default. Use open to allow inheritance and overriding.

open class Animal(val name: String) {
    open fun makeSound() {
        println("$name makes a sound")
    }
    
    fun eat() {
        println("$name is eating")
    }
}

class Dog(name: String) : Animal(name) {
    override fun makeSound() {
        println("$name barks")
    }
}

class Cat(name: String) : Animal(name) {
    override fun makeSound() {
        println("$name meows")
    }
}

fun main() {
    val animals: List<Animal> = listOf(Dog("Rex"), Cat("Whiskers"))
    
    for (animal in animals) {
        animal.makeSound()  // Polymorphic call
        animal.eat()        // Inherited method
    }
}

Output:

Rex barks
Rex is eating
Whiskers meows
Whiskers is eating

The eat() method is inherited. The makeSound() method is overridden in each subclass. Polymorphism dispatches to the correct implementation at runtime.

Override Rules

override is mandatory when redefining an open member. Overridden methods remain open unless marked final.

open class Vehicle {
    open val wheels: Int = 0
    open fun start() = println("Vehicle starting")
    open fun stop() = println("Vehicle stopping")
    fun honk() = println("Beep!")  // Cannot be overridden (not open)
}

open class Car : Vehicle() {
    override val wheels: Int = 4
    
    override fun start() {
        println("Car engine starting")
        super.start()
    }
    
    override fun stop() {
        println("Car stopping")
        super.stop()
    }
}

class SportsCar : Car() {
    override fun start() {
        println("Sports car roaring to life")
        super.start()
    }
    
    // Stop is still open, so we can override again
    override fun stop() {
        println("Sports car braking hard")
        super.stop()
    }
}

fun main() {
    val car: Vehicle = SportsCar()
    car.start()
    car.stop()
    car.honk()
}

Output:

Sports car roaring to life
Car engine starting
Vehicle starting
Sports car braking hard
Car stopping
Vehicle stopping
Beep!

Each override calls super to chain up the hierarchy. honk() cannot be overridden because it is not marked open.

Abstract Classes

Abstract classes define incomplete implementations that subclasses must complete.

abstract class Shape(val name: String) {
    // Abstract property (must be overridden)
    abstract val area: Double
    
    // Abstract method (must be implemented)
    abstract fun perimeter(): Double
    
    // Concrete method (inherited)
    fun description() = "$name: area=$area, perimeter=${perimeter()}"
}

class Circle(radius: Double) : Shape("Circle") {
    private val r = radius
    
    override val area: Double get() = Math.PI * r * r
    
    override fun perimeter(): Double = 2 * Math.PI * r
}

class Rectangle(width: Double, height: Double) : Shape("Rectangle") {
    private val w = width
    private val h = height
    
    override val area: Double get() = w * h
    
    override fun perimeter(): Double = 2 * (w + h)
}

fun main() {
    val shapes: List<Shape> = listOf(Circle(5.0), Rectangle(3.0, 4.0))
    
    for (shape in shapes) {
        println(shape.description())
    }
}

Output:

Circle: area=78.53981633974483, perimeter=31.41592653589793
Rectangle: area=12.0, perimeter=14.0

Abstract classes cannot be instantiated directly. Subclasses must implement all abstract members.

Sealed Classes

Sealed classes restrict inheritance to a known set of subclasses defined in the same file.

sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Error(val message: String, val code: Int) : ApiResult<Nothing>()
    data object Loading : ApiResult<Nothing>()
}

fun fetchUserData(): ApiResult<String> {
    // Simulated API call
    return ApiResult.Success("User profile data")
}

fun handleResult(result: ApiResult<String>) {
    when (result) {
        is ApiResult.Success -> println("Data: ${result.data}")
        is ApiResult.Error -> println("Error ${result.code}: ${result.message}")
        ApiResult.Loading -> println("Loading...")
    }
    // No else needed: when is exhaustive
}

fun main() {
    val result = fetchUserData()
    handleResult(result)  // Output: Data: User profile data
    
    handleResult(ApiResult.Error("Not found", 404))  // Output: Error 404: Not found
}

Output: The when expression covers all possible subclasses. Adding a new subclass causes a compilation error until the when is updated.

Sealed classes are ideal for representing states (loading, success, error) and command patterns.

Polymorphism

Polymorphism allows objects of different types to be treated uniformly through a common interface.

open class PaymentMethod(val name: String) {
    open fun processPayment(amount: Double): Boolean {
        println("Processing $amount via $name")
        return true
    }
}

class CreditCard(number: String) : PaymentMethod("CreditCard") {
    override fun processPayment(amount: Double): Boolean {
        println("Charging $$amount to card ending in ${number.takeLast(4)}")
        return true
    }
}

class PayPal(email: String) : PaymentMethod("PayPal") {
    override fun processPayment(amount: Double): Boolean {
        println("Processing $$amount via PayPal account $email")
        return true
    }
}

class Cash : PaymentMethod("Cash") {
    override fun processPayment(amount: Double): Boolean {
        println("Accepting $$amount in cash")
        return true
    }
}

fun processCheckout(payment: PaymentMethod, total: Double) {
    payment.processPayment(total)
}

fun main() {
    val payments = listOf(
        CreditCard("1234-5678-9012-3456"),
        PayPal("user@example.com"),
        Cash()
    )
    
    for (payment in payments) {
        processCheckout(payment, 50.0)
    }
}

Output: Each payment method processes differently through the same polymorphic interface.

Property Overriding

Properties can be overridden just like methods.

open class Base {
    open val value: String = "Base"
    open val count: Int get() = 10
}

class Derived : Base() {
    // Override with a constant
    override val value: String = "Derived"
    
    // Override with a getter
    override val count: Int
        get() = super.count * 2
}

fun main() {
    val obj: Base = Derived()
    println(obj.value)  // Output: Derived
    println(obj.count)  // Output: 20
}

Output: Property overrides work through polymorphism. The getter is dispatched dynamically.

Common Mistakes

  1. Forgetting the open keyword: Classes and methods are final by default. Attempting to extend a non-open class causes a compilation error.

  2. Overriding without override: The override modifier is mandatory. Omitting it causes a compilation error. This prevents accidental overriding.

  3. Calling open methods in constructors: When a constructor calls an open method, the subclass implementation runs before the subclass is fully initialized. This can lead to bugs.

  4. Overusing inheritance: Favor Composition Over Inheritance. If the relationship is "has a" rather than "is a", use delegation or interfaces instead of extending a class.

  5. Ignoring sealed class exhaustiveness: When using sealed classes in a when expression, adding a new subclass requires updating all when blocks. The compiler enforces this, making Refactoring safe.

  6. Not using data object: For sealed subclasses with no data, use data object instead of class. This provides proper equals, hashCode, and toString.

Practice Questions

  1. Why must classes and methods be marked open to allow inheritance?

Answer: Kotlin makes classes and methods final by default to prevent accidental inheritance. The open keyword makes the design decision explicit, following the principle of favoring composition over inheritance.

  1. What is the difference between an abstract class and an open class?

Answer: Abstract classes cannot be instantiated and may have abstract members without implementation. Open classes can be instantiated and all members must have implementations unless they are also abstract.

  1. What is a sealed class and when should you use it?

Answer: A sealed class restricts inheritance to a fixed set of subclasses defined in the same file. Use it for representing states, command patterns, or any restricted union type.

  1. How does polymorphism work in Kotlin?

Answer: Polymorphism allows a variable of a supertype to refer to any subtype. Method calls are dispatched to the actual runtime type through dynamic dispatch.

  1. Challenge: Design a shape hierarchy with an abstract Shape class, concrete Circle and Rectangle subclasses, and a sealed class for shape operations (CalculateArea, CalculatePerimeter, Describe). Use a when expression to perform operations polymorphically.

Answer:

abstract class Shape {
    abstract val area: Double
    abstract fun perimeter(): Double
}

class Circle(val radius: Double) : Shape() {
    override val area: Double get() = Math.PI * radius * radius
    override fun perimeter(): Double = 2 * Math.PI * radius
}

class Rectangle(val w: Double, val h: Double) : Shape() {
    override val area: Double get() = w * h
    override fun perimeter(): Double = 2 * (w + h)
}

sealed class ShapeOperation {
    data object CalculateArea : ShapeOperation()
    data object CalculatePerimeter : ShapeOperation()
    data class Describe(val name: String) : ShapeOperation()
}

fun applyOperation(shape: Shape, op: ShapeOperation): String = when (op) {
    ShapeOperation.CalculateArea -> "Area: ${shape.area}"
    ShapeOperation.CalculatePerimeter -> "Perimeter: ${shape.perimeter()}"
    is ShapeOperation.Describe -> "${op.name}: area=${shape.area}, perimeter=${shape.perimeter()}"
}

fun main() {
    val circle = Circle(5.0)
    println(applyOperation(circle, ShapeOperation.CalculateArea))
    println(applyOperation(circle, ShapeOperation.Describe("My Circle")))
}

Mini Project

Build a document processing system using inheritance and sealed classes. Requirements:

  • Abstract Document class with open and display() methods
  • Concrete classes: PDFDocument, WordDocument, SpreadsheetDocument
  • Each with specific open logic
  • Sealed class DocumentOperation with Open, Print, ExportAsPDF, and Close
  • A DocumentProcessor that takes a Document and an operation, executing it with exhaustive when
  • Track the active document type and validate that certain operations are only valid for certain types

This project applies inheritance, polymorphism, sealed classes, and exhaustive state management.

FAQ

Can I inherit from multiple classes in Kotlin?

No, Kotlin supports single class inheritance. For multiple type conformance, use interfaces which support multiple inheritance.

What does the 'super' keyword do?

super references the parent class. Use super.methodName() to call the parent's implementation from an override.

Can I mark an override as final to prevent further overriding?

Yes. Add the final keyword to the overridden member: 'final override fun start()'.

What is the difference between sealed class and enum class?

Sealed classes can have multiple instances with different state and data. Enum classes have a fixed set of singleton instances. Sealed classes are more flexible.

Can I have a private primary constructor in an abstract class?

Yes. An abstract class can have a private constructor. Subclasses defined in the same file can still extend it.

What's Next

After mastering inheritance, learn interfaces for defining contracts. You can also explore data classes for concise model definitions.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro