Kotlin Inheritance and Interfaces

Reuse and extend behaviour with open classes, override and super, define contracts with interfaces, share code with abstract classes, and use polymorphism.

Lesson 15 of 43Intermediate17 min read

Often several kinds of thing share a lot in common. A savings account and a current account are both accounts. A circle and a square are both shapes. A card payment and a voucher payment are both payments. Inheritance lets a class build on another, reusing its properties and methods and changing only what's different.

Kotlin is deliberately cautious here: classes are final by default, meaning nothing can inherit from them unless you mark them open. This prevents accidental, fragile inheritance chains, a common source of bugs in large codebases. For describing what classes can do, Kotlin prefers interfaces: contracts that any class can promise to fulfil.

In this lesson you'll create open classes and subclasses, override methods and call the parent's version with super, use abstract classes for shared code, define and implement interfaces, and treat different classes the same way through polymorphism.

open classes and override

Kotlin
open class Animal(val name: String) {
    open fun sound() = "..."
    fun describe() = "$name says ${sound()}"
}

class Dog(name: String) : Animal(name) {
    override fun sound() = "Woof"
}
  • open allows a class to be inherited and a method to be overridden.
  • : Animal(name) declares the parent and passes arguments to its constructor.
  • override is required when replacing a parent's method. Misspell the name and the compiler tells you, instead of silently creating a new method.
  • describe() isn't open, so subclasses can't change it, but it calls sound(), which they can.

super and abstract classes

Inside an overriding method, super.method() calls the parent's version, so you can add to it rather than replace it entirely.

An abstract class can't be created on its own. It can contain normal code plus abstract members with no body, which every subclass must provide:

Kotlin
abstract class Shape {
    abstract fun area(): Double
    fun describe() = "${this::class.simpleName} with area ${"%.2f".format(area())}"
}

Abstract members are automatically open, so subclasses just write override fun area() = ....

Interfaces

An interface describes abilities: a set of functions and properties a class promises to provide.

Kotlin
interface Payable {
    val label: String
    fun pay(amount: Double): Boolean
    fun receipt(amount: Double) = "$label: £$amount"   // default implementation
}
  • A class can implement many interfaces, but extend only one class: class Card : Payable, Refundable.
  • Interfaces can include default method implementations, as with receipt above.
  • Properties in interfaces have no stored value; the implementing class provides them.
  • Interfaces can't hold state, which keeps them simple and flexible.

Polymorphism and type checks

Code written for a parent type works with every subclass:

Kotlin
val animals: List<Animal> = listOf(Dog("Rex"), Cat("Tom"))
for (a in animals) println(a.describe())

Each object runs its own version of sound(). That's polymorphism: one piece of code, many behaviours. When you need subclass-specific features, check with is, after which the value is smart cast: if (a is Dog) a.fetch().

Kotlin Inheritance Syntax

Inheritance syntax

open class Parent(val x: Int) {
    open fun f() { }
}
class Child(x: Int) : Parent(x) {
    override fun f() { super.f() }
}
abstract class Base { abstract fun g(): Int }
interface Ability { fun act(); fun extra() = "default" }
class Thing : Base(), Ability {
    override fun g() = 1
    override fun act() { }
}

Kotlin Inheritance Summary

  • Kotlin classes and methods are final by default; open allows inheritance and overriding.
  • class Child(...) : Parent(...) inherits; override replaces an open member; super calls the parent's version.
  • Abstract classes can't be instantiated and declare abstract members subclasses must implement.
  • Interfaces define abilities, can have default methods, and a class can implement many.
  • Polymorphism lets one piece of code work with many subclasses; is checks and smart casts.

Kotlin Inheritance Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Accounts with shared and special behaviour

Kotlin
open class Account(val owner: String, var balance: Double) {
    open fun monthEnd() { }
    fun summary() = "$owner: £${"%.2f".format(balance)}"
}

class Savings(owner: String, balance: Double, private val rate: Double) : Account(owner, balance) {
    override fun monthEnd() {
        balance += balance * rate
    }
}

class Current(owner: String, balance: Double) : Account(owner, balance) {
    override fun monthEnd() {
        super.monthEnd()
        balance -= 2.0
    }
}

fun main() {
    val accounts = listOf(Savings("Sam", 1000.0, 0.01), Current("Lea", 500.0))
    accounts.forEach { it.monthEnd() }
    accounts.forEach { println(it.summary()) }
}
Output
Sam: £1010.00
Lea: £498.00
Line-by-line explanation
  1. Lines 1 to 2Account is open, and monthEnd is open so each kind of account can customise it.
  2. Line 6Savings passes owner and balance on to the parent constructor.
  3. Lines 13 to 16Current calls the parent's version first, then charges a fee.
  4. Line 21The same call runs a different monthEnd for each object: polymorphism.
Example 2

An abstract class for shapes

Kotlin
import kotlin.math.PI

abstract class Shape {
    abstract val name: String
    abstract fun area(): Double
    fun describe() = "$name with area ${"%.2f".format(area())}"
}

class Circle(private val r: Double) : Shape() {
    override val name = "Circle"
    override fun area() = PI * r * r
}

class Rectangle(private val w: Double, private val h: Double) : Shape() {
    override val name = "Rectangle"
    override fun area() = w * h
}

fun main() {
    val shapes = listOf(Circle(1.0), Rectangle(3.0, 4.0), Circle(2.0))
    shapes.forEach { println(it.describe()) }
    println("Total: ${"%.2f".format(shapes.sumOf { it.area() })}")
}
Output
Circle with area 3.14
Rectangle with area 12.00
Circle with area 12.57
Total: 27.71
Line-by-line explanation
  1. Line 3Shape is abstract, so Shape() on its own would be a compile error.
  2. Lines 4 to 5Every subclass must provide a name and an area.
  3. Line 6Shared code uses the abstract members without knowing the exact shape.
  4. Line 22sumOf works on the parent type, adding every shape's area.
Example 3

Interfaces for payments

Kotlin
interface Payment {
    val label: String
    fun charge(amount: Double): Boolean
    fun receipt(amount: Double) = "$label payment of £$amount"
}

class Card : Payment {
    override val label = "Card"
    override fun charge(amount: Double) = amount <= 500
}

class Voucher(private var balance: Double) : Payment {
    override val label = "Voucher"
    override fun charge(amount: Double): Boolean {
        if (amount > balance) return false
        balance -= amount
        return true
    }
}

fun checkout(payment: Payment, amount: Double) {
    val result = if (payment.charge(amount)) "approved" else "declined"
    println("${payment.receipt(amount)}: $result")
}

fun main() {
    checkout(Card(), 89.99)
    checkout(Voucher(25.0), 40.0)
}
Output
Card payment of £89.99: approved
Voucher payment of £40.0: declined
Line-by-line explanation
  1. Line 4A default implementation shared by every payment type.
  2. Line 7A class implements an interface with a colon and no brackets, because interfaces have no constructor.
  3. Line 21checkout accepts any Payment, so new payment types need no changes here.

Common Kotlin Inheritance Mistakes

Inheriting from a final class

Don't
class Animal(val name: String)
class Dog(name: String) : Animal(name)    // error: This type is final, so it cannot be inherited from
Do
open class Animal(val name: String)
class Dog(name: String) : Animal(name)

Forgetting override

Don't
class Dog : Animal("Rex") {
    fun sound() = "Woof"     // error: 'sound' hides member of supertype and needs 'override'
}
Do
class Dog : Animal("Rex") {
    override fun sound() = "Woof"
}

Brackets after an interface name

Don't
class Card : Payment() { }          // error: interfaces have no constructor
Do
class Card : Payment { }

Kotlin Inheritance in Real Projects

Inheritance and interfaces in practice

Android screens extend framework classes such as Activity and ViewModel, while app code is usually organised around interfaces like UserRepository, with one implementation that talks to the network and another fake one for tests. This "program to an interface" approach is what lets large teams swap pieces without breaking everything around them.

Kotlin Inheritance FAQ

What is the difference between an interface and an abstract class in Kotlin?

An abstract class can hold state, such as properties with stored values and constructor parameters, and a class can extend only one. An interface can't store state, but a class can implement as many as it likes. Use an interface to describe an ability, and an abstract class when subclasses share real data and code.

Why are Kotlin classes final by default?

So that a class can only be extended when its author has planned for it. Inheriting from a class that wasn't designed for it can break in subtle ways when the parent changes. Add open to the class, and to each member you want subclasses to override.

How do I stop a subclass from overriding a method again?

An override is itself open, so grandchildren can override it too. Write final override fun sound() = "Tweet" to stop the chain there. Any further subclass that tries to override sound gets a compile error saying the member is final.

What happens if a class implements two interfaces with the same method?

If both interfaces provide a default implementation of the same function, the compiler makes you override it in the class. Inside your override, you can call either version with super<A>.hello() or super<B>.hello(), or combine both.

Can I override a property in Kotlin?

Yes, if the parent marks it open or abstract. Write override val legs = 2 in the subclass, or override it in the constructor with override val. A val can be overridden with a var, but not the other way round.

Kotlin Inheritance Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the class so other classes can inherit from it.

 class Vehicle(val wheels: Int)
Multiple choiceeasy

How many classes can a Kotlin class extend?

Predict the outputmedium

What does this print?

open class A { open fun hi() = "A" }
class B : A() { override fun hi() = "B" + super.hi() }
println(B().hi())
Multiple choicemedium

What must a subclass of an abstract class do?

Multiple choicehard

Why does Kotlin make classes final by default?

Kotlin Inheritance Quiz

5 questions to check what you learned.

  1. Which keyword replaces a parent's method?

    • replace
    • super
    • override
    • open
  2. Can an interface have a function with a body?

    • Yes, as a default implementation
    • No
    • Only if it's private
    • Only in abstract classes
  3. What is polymorphism?

    • Having many constructors
    • Copying objects
    • Using many interfaces at once
    • One piece of code working with objects of different classes through a shared type, each behaving its own way
  4. What does super.monthEnd() call?

    • The subclass's own version
    • The parent class's version of the method
    • A static function
    • Nothing
  5. Why is class Card : Payment written without brackets after Payment?

    • Brackets are optional everywhere
    • Card has no properties
    • Payment is an interface, which has no constructor to call
    • It's a data class