Free Handbook · Every example compiled & verified

Inheritance & Interfaces

Kotlin inheritance done right: open and override, abstract classes, interfaces with default methods, polymorphism, delegation with by, and type checks.

0 / 136 lessons🔥 0 day streak
ShareXLinkedIn

Module 07 · what you'll be able to do

  • Explain why Kotlin classes are final by default and use open, override and super correctly
  • Choose between an abstract class and an interface, and give interfaces default methods and properties
  • Resolve a conflict between two interface defaults with super<A>
  • Trace which function runs when a call goes through a supertype reference
  • Prefer composition with by delegation, and check types safely with is, as? and smart casts
01

open, override and super

In Java every class can be extended unless you mark it final. Kotlin flips that: classes and members are final by default. To allow a subclass you write open class, and each function or property a subclass may replace must also be open. The subclass marks its replacement with override — a required keyword, so you can never override by accident or fail to override because of a typo.

kotlinMain.kt
open class Notifier(val channel: String) {
    open fun format(msg: String) = "[$channel] $msg"
    fun send(msg: String) = println(format(msg))   // final: not overridable
}

class UrgentNotifier : Notifier("pager") {
    override fun format(msg: String) = super.format(msg.uppercase()) + " !!!"
}

fun main() {
    Notifier("email").send("build passed")
    UrgentNotifier().send("disk full")
}
Outputcompiled & run with real Kotlin
[email] build passed
[pager] DISK FULL !!!

super.format(...) reuses the parent's version instead of copying it.

Your turn

Add an open val prefix = "" to Notifier, use it in format, and override it in the subclass as override val prefix = "URGENT ".

The subclass header : Notifier("pager") both names the parent and calls its constructor — the parent is always fully built before the child's own initialisers run. An override is itself open to further subclasses; write final override to stop the chain.

Error you will hit

Extending a class that is not open

kotlin
class Animal(val name: String)
class Dog(name: String) : Animal(name)

fun main() = println(Dog("Rex").name)
Main.kt:2:27: error: this type is final, so it cannot be extended.
class Dog(name: String) : Animal(name)
                          ^^^^^^
Why the compiler said that

Kotlin classes are final unless marked open. The designers chose this because a class that was never designed for extension breaks in subtle ways when someone subclasses it (the "fragile base class" problem).

The fix

Add open to the parent if it really is designed to be extended. Often the better answer is an interface or composition instead of inheritance.

kotlin
open class Animal(val name: String)
class Dog(name: String) : Animal(name)

fun main() = println(Dog("Rex").name)
Error you will hit

Overriding a member that is not open

kotlin
open class Base {
    fun greet() = "hi"
}
class Child : Base() {
    override fun greet() = "hello"
}

fun main() = println(Child().greet())
Main.kt:5:5: error: 'greet' in 'Base' is final and cannot be overridden.
    override fun greet() = "hello"
    ^^^^^^^^
Why the compiler said that

Opening the class does not open its members. Each function and property is final until it is marked open (or abstract) on its own.

The fix

Mark the function open in the parent.

kotlin
open class Base {
    open fun greet() = "hi"
}
class Child : Base() {
    override fun greet() = "hello"
}

fun main() = println(Child().greet())
Never call an open member from a constructor
The parent is constructed first. If its init calls an open function the child overrides, the child's version runs before the child's own properties are initialised — and a non-null String property can be read as null. The example below shows it happening.
kotlinMain.kt
open class Base {
    init { println("Base sees: ${describe()}") }
    open fun describe() = "base"
}

class Child : Base() {
    private val name: String = "child"
    override fun describe() = "name=$name"
}

fun main() {
    val c = Child()
    println("After construction: ${c.describe()}")
}
Outputcompiled & run with real Kotlin
Base sees: name=null
After construction: name=child

kotlinc compiles this without complaint; IntelliJ flags it with an inspection. Treat that highlight as an error.

02

Abstract classes

An abstract class is a parent that is incomplete on purpose: it declares abstract members with no body, and every concrete subclass must implement them. It can also hold state (constructor properties, fields) and finished methods that use the abstract ones — the template method pattern. Abstract classes and abstract members are implicitly open.

kotlinMain.kt
abstract class Report(val title: String) {
    abstract fun rows(): List<String>
    open fun footer() = "-- end --"

    fun render(): String = buildString {       // template method
        appendLine("== $title ==")
        rows().forEachIndexed { i, r -> appendLine("${i + 1}. $r") }
        append(footer())
    }
}

class SalesReport(private val sales: Map<String, Int>) : Report("Sales") {
    override fun rows() = sales.map { (who, amt) -> "$who: $amt" }
    override fun footer() = "total ${sales.values.sum()}"
}

fun main() {
    println(SalesReport(mapOf("north" to 120, "south" to 80)).render())
}
Outputcompiled & run with real Kotlin
== Sales ==
1. north: 120
2. south: 80
total 200
Your turn

Write an ErrorsReport that lists three error strings and keeps the default footer.

Error you will hit

Instantiating an abstract class

kotlin
abstract class Shape {
    abstract fun area(): Double
}

fun main() {
    val s = Shape()
    println(s.area())
}
Main.kt:6:13: error: cannot create an instance of an abstract class.
    val s = Shape()
            ^^^^^^^
Why the compiler said that

area() has no body, so a plain Shape would have nothing to run when you called it. Only concrete subclasses can be created.

The fix

Create a subclass that implements every abstract member, or an anonymous one on the spot with an object expression.

kotlin
abstract class Shape {
    abstract fun area(): Double
}
class Square(val side: Double) : Shape() {
    override fun area() = side * side
}

fun main() {
    val s: Shape = Square(3.0)
    println(s.area())
}
03

Interfaces with default methods and properties

An interface describes what a type can do. Unlike an abstract class, it has no constructor and no stored state, and a class can implement as many interfaces as it likes (but extend only one class). Interfaces can still contain default method bodies and properties — either abstract ones the class must provide, or ones with a getter computed from other members.

kotlinMain.kt
interface Identifiable {
    val id: String                          // abstract: implementer provides it
    val shortId: String get() = id.take(4)  // computed from id
}

interface Auditable {
    fun describe(): String
    fun audit() = println("audit: ${describe()}")   // default method
}

class Invoice(override val id: String, val amount: Int) : Identifiable, Auditable {
    override fun describe() = "invoice $shortId for $amount"
}

fun main() {
    val inv = Invoice("INV-20931", 499)
    inv.audit()
    val things: List<Identifiable> = listOf(inv, object : Identifiable { override val id = "TMP-1" })
    println(things.map { it.shortId })
}
Outputcompiled & run with real Kotlin
audit: invoice INV- for 499
[INV-, TMP-]

abstract class

  • Can have a constructor and stored state
  • A class extends at most one
  • Members can be protected
  • Use for a shared base with real behaviour and fields

interface

  • No constructor, no backing fields
  • A class implements any number
  • Members are public (private helpers allowed)
  • Use for a capability: Comparable, Closeable, Auditable
Code against interfaces
In production code, services depend on interfaces (UserRepository) and receive the real implementation (PostgresUserRepository) through the constructor. Tests pass an in-memory fake instead. This is the single most common reason you will write an interface at work.
04

Resolving diamond conflicts with super<A>

Because a class can implement several interfaces, two of them may bring a default method with the same signature. Kotlin will not silently pick one. The class must override the method, and inside the override it can call each parent's version explicitly with super<InterfaceName>.method().

Error you will hit

Two interfaces, one method name

kotlin
interface Printer { fun hello() = "Printer" }
interface Scanner { fun hello() = "Scanner" }

class Copier : Printer, Scanner

fun main() = println(Copier().hello())
Main.kt:4:1: error: class 'Copier' must override 'hello' because it inherits multiple interface methods for it.
class Copier : Printer, Scanner
^^^^^^^^^^^^
Why the compiler said that

Both parents supply a body for hello(), so a call on a Copier is ambiguous. Rather than guess based on declaration order, the compiler makes you decide.

The fix

Override hello() in the class and combine or choose the parents' versions with super<Printer> and super<Scanner>.

kotlin
class Copier : Printer, Scanner {
    override fun hello() = super<Printer>.hello() + "+" + super<Scanner>.hello()
}
kotlinMain.kt
interface Printer {
    fun hello() = "Printer"
    fun print(doc: String) = "printing $doc"
}
interface Scanner {
    fun hello() = "Scanner"
    fun scan() = "scanning"
}

class Copier : Printer, Scanner {
    override fun hello() = super<Printer>.hello() + "+" + super<Scanner>.hello()
    fun copy(doc: String) = "${scan()} then ${print(doc)}"
}

fun main() {
    val c = Copier()
    println(c.hello())
    println(c.copy("contract.pdf"))
    val p: Printer = c
    println(p.hello())       // still the Copier override
}
Outputcompiled & run with real Kotlin
Printer+Scanner
scanning then printing contract.pdf
Printer+Scanner

Methods only one parent defines (print, scan) are inherited with no conflict.

05

Polymorphism: which function actually runs

A variable has a static type (what the compiler knows, for example Shape) and the object it points at has a runtime type (for example Circle). The static type decides which calls compile; the runtime type decides which override runs. This late decision is called dynamic dispatch, and it is what lets one loop handle many kinds of objects.

kotlinMain.kt
open class Employee(val name: String) {
    open fun pay(): Int = 1000
    fun payslip() = "$name: ${pay()}"
}

class Manager(name: String, private val reports: Int) : Employee(name) {
    override fun pay() = super.pay() + reports * 200
}

class Contractor(name: String, private val hours: Int) : Employee(name) {
    override fun pay() = hours * 50
}

fun main() {
    val staff: List<Employee> = listOf(
        Employee("Ada"), Manager("Linus", 3), Contractor("Grace", 10),
    )
    staff.forEach { println(it.payslip()) }
    println(staff.sumOf { it.pay() })
}
Outputcompiled & run with real Kotlin
Ada: 1000
Linus: 1600
Grace: 500
3100
Your turn

Add an Intern that earns half the base pay using super.pay() / 2. The loop in main needs no change.

VisualizeDispatching payslip() on a ManagerStep 1 / 7
open class Employee(val name: String) {
open fun pay(): Int = 1000
fun payslip() = "$name: ${pay()}"
}
class Manager(name: String, val reports: Int) : Employee(name) {
override fun pay() = super.pay() + reports * 200
}
val e: Employee = Manager("Linus", 3)
println(e.payslip())
Line 8

The static type of e is Employee; the object is a Manager.

Variables now
eManager(Linus, reports=3)
All 7 steps as a table
StepLineWhat happenedVariables now
18The static type of e is Employee; the object is a Manager.e = Manager(Linus, reports=3)
29payslip() is declared (and final) in Employee, so that body runs.
33Inside it, pay() is an open call. The JVM looks at the runtime type — Manager — and picks Manager.pay, not Employee.pay.
46super.pay() jumps up to the parent's body on purpose.
52The parent returns 1000.
66Manager adds 3 * 200 and returns the sum.pay() = 1600
73The template builds the string.
Extensions do not dispatch
Only member functions are chosen by runtime type. An extension function is picked by the static type at compile time, so fun Employee.badge() and fun Manager.badge() called on a variable typed Employee always run the Employee one. See Module 09 (Generics & Extensions).
06

Type checks and casts: is, as, as?

x is Type checks the runtime type and returns a Boolean. After a successful check the compiler smart-casts x, so you can use Type's members without an explicit cast — in an if, a when branch, or after an early return on !is. x as Type is an explicit cast that throws if wrong; x as? Type returns null instead.

kotlinMain.kt
open class Animal(val name: String)
class Dog(name: String) : Animal(name) { fun fetch() = "$name fetches" }
class Cat(name: String) : Animal(name) { fun purr() = "$name purrs" }

fun act(a: Animal): String = when (a) {
    is Dog -> a.fetch()          // smart cast to Dog
    is Cat -> a.purr()           // smart cast to Cat
    else -> "${a.name} idles"
}

fun main() {
    val zoo = listOf(Dog("Rex"), Cat("Tom"), Animal("Generic"))
    zoo.forEach { println(act(it)) }

    val maybeDog = zoo[1] as? Dog
    println(maybeDog?.fetch() ?: "not a dog")
    println(zoo.filterIsInstance<Cat>().map { it.name })
}
Outputcompiled & run with real Kotlin
Rex fetches
Tom purrs
Generic idles
not a dog
[Tom]
Error you will hit

ClassCastException from an unsafe as

kotlin
open class Animal
class Dog : Animal()
class Cat : Animal()

fun main() {
    val pet: Animal = Cat()
    val dog = pet as Dog
    println(dog)
}
Exception in thread "main" java.lang.ClassCastException: class Cat cannot be cast to class Dog (Cat and Dog are in unnamed module of loader 'app')
	at MainKt.main(Main.kt:7)
	at MainKt.main(Main.kt)
Why the compiler said that

as tells the compiler "trust me". The compiler accepts it because a Dog is a kind of Animal, but at runtime the object is a Cat, so the JVM throws on line 7.

The fix

Use is with a smart cast, or as? and handle the null. A plain as belongs only where a wrong type really is a bug that should crash.

kotlin
fun main() {
    val pet: Animal = Cat()
    val dog = pet as? Dog
    println(dog ?: "pet is not a dog")
}
A long is-chain is a design smell
If you find yourself writing is Dog, is Cat, is Parrot in many places, either move the behaviour into an overridden method (polymorphism), or, when the set of types is fixed, make the parent sealed so the compiler checks every when for you — see Module 08 (Data, Sealed & Enum Classes).
07

Delegation with by: composition over inheritance

Inheritance couples you to every detail of the parent. The alternative is composition: hold an object and forward calls to it. Normally that means writing one forwarding method per interface method; Kotlin writes them for you with by. class CountingSet(inner: MutableSet<T>) : MutableSet<T> by inner implements the whole interface by forwarding to inner, and you override only the calls you care about.

kotlinMain.kt
class CountingSet<T>(
    private val inner: MutableSet<T> = mutableSetOf(),
) : MutableSet<T> by inner {
    var attempts = 0
        private set

    override fun add(element: T): Boolean {
        attempts++
        return inner.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        attempts += elements.size
        return inner.addAll(elements)
    }

    override fun toString() = inner.toString()
}

fun main() {
    val s = CountingSet<String>()
    s.add("a")
    s.addAll(listOf("a", "b", "c"))
    println("${s.attempts} attempts, ${s.size} stored: $s")
    println("b" in s)            // contains() is forwarded untouched
}
Outputcompiled & run with real Kotlin
4 attempts, 3 stored: [a, b, c]
true

Only add, addAll and toString are hand-written; every other MutableSet member is a generated forward.

by does not forward toString, equals or hashCode
Class delegation forwards the interface members only. toString, equals and hashCode come from Any, so without the override above, $s would print CountingSet@1b6d3586. Forward them yourself when the wrapper should behave like its contents.

Delegated properties

The same keyword works on a property: val x by something hands the getter (and setter, for var) to a delegate object. Module 09 (Generics & Extensions) introduces lazy, observable and by map. Two more are worth knowing: Delegates.vetoable rejects a new value when its callback returns false, and you can write your own delegate by implementing ReadWriteProperty — a reusable getter/setter you attach to any property.

kotlinMain.kt
import kotlin.properties.Delegates
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class Trimmed : ReadWriteProperty<Any?, String> {
    private var value = ""
    override fun getValue(thisRef: Any?, property: KProperty<*>) = value
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("setting ${property.name}")
        this.value = value.trim()
    }
}

class Profile {
    var displayName: String by Trimmed()
    var age: Int by Delegates.vetoable(0) { _, _, new -> new in 0..150 }
    val slug: String by lazy { displayName.lowercase().replace(' ', '-') }
}

fun main() {
    val p = Profile()
    p.displayName = "   Ada Lovelace  "
    p.age = 36
    p.age = -4                    // vetoed: keeps 36
    println("[${p.displayName}] ${p.age} ${p.slug}")
}
Outputcompiled & run with real Kotlin
setting displayName
[Ada Lovelace] 36 ada-lovelace
Your turn

Write a Clamped(min, max) delegate for Int that stores value.coerceIn(min, max) instead of rejecting.

open
Allows a class to be subclassed or a member to be overridden. Kotlin classes and members are final without it.
override
Required keyword on a member that replaces a parent's open or abstract member.
super
Calls the parent's version of a member; super<A> picks one parent when several interfaces supply it.
abstract class
A class that cannot be instantiated and may declare members without bodies for subclasses to implement.
interface
A contract of functions and properties with optional default bodies and no stored state; a class can implement many.
dynamic dispatch
Choosing which override runs from the object's runtime type, not the variable's declared type.
smart cast
After an is check, the compiler treats the value as the checked type automatically.
safe cast (as?)
A cast that returns null instead of throwing ClassCastException.
class delegation
: Interface by obj — the compiler generates forwarding methods to obj.
delegated property
A property whose get/set are handled by another object, such as lazy, vetoable or your own ReadWriteProperty.
Quick check

val e: Employee = Manager("Linus", 3). Both classes define pay(), Manager with override. What does e.pay() run?

Quick check

Which of these can an interface in Kotlin NOT have?

Frequently asked questions

Why are Kotlin classes final by default?
Because a class that was not designed for extension can break when it is subclassed: overrides can depend on internal call order that later changes. Making extension opt-in with open forces the author to decide which parts are safe to override.
What is the difference between an abstract class and an interface in Kotlin?
An abstract class can have a constructor, stored state and protected members, and a class can extend only one. An interface has no constructor or stored state, but can have default methods and computed properties, and a class can implement many.
What does the by keyword do in Kotlin?
On a class, by implements an interface by forwarding every call to another object, so you override only what you need. On a property, by hands its getter and setter to a delegate object such as lazy or Delegates.observable.

Finish the Kotlin handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.