Skip to content
CalliCoder

Kotlin Inheritance and Method Overriding

Published Updated Kotlin 14 min read

Classes are final unless opened, override is mandatory and stays open unless finalled, and a class implementing two interfaces with the same default must resolve it explicitly.

Kotlin inverts Java’s default: a class cannot be extended and a method cannot be overridden unless the author says so. That single change eliminates the “designed for extension by accident” problem, and it introduces three keywords (open, override and final override), that together describe exactly how far a hierarchy is meant to go.

Written against Kotlin 1.9.

open is required

class Base                     // final — cannot be extended
open class Base                // extendable

open class Vehicle(val wheels: Int) {
    open fun describe(): String = "Vehicle with $wheels wheels"
    fun identify(): String = "Vehicle"          // final — cannot be overridden
}

class Car : Vehicle(4) {
    override fun describe(): String = "Car"
}

open is needed on the class and on each member intended to be overridable. Marking the class open does not open its members.

The default follows Effective Java’s advice, “design and document for inheritance or else prohibit it”, and makes the prohibition the default rather than the exception. In practice it means a library author has to decide deliberately what is an extension point, and a caller cannot subclass their way around an implementation detail.

Note the syntax: : Vehicle(4) both declares the supertype and calls its constructor. There is no separate extends/implements distinction: a colon covers both, and an interface appears in the same list with no constructor call.

override is mandatory, and stays open

open class Car : Vehicle(4) {
    override fun describe(): String = "Car"           // implicitly open
}

class SportsCar : Car() {
    override fun describe(): String = "Sports car"    // legal
}

An overriding member is itself open, which is easy to miss. Stopping the chain takes final:

open class Car : Vehicle(4) {
    final override fun describe(): String = "Car"     // no further overriding
}

override being required rather than advisory removes a whole class of Java bugs. A member that does not actually override anything is a compile error rather than a silently new method, so a renamed or re-signatured base method breaks at the subclass instead of at runtime.

Calling the parent

open class Vehicle {
    open fun describe(): String = "A vehicle"
}

class Car : Vehicle() {
    override fun describe(): String = super.describe() + " — specifically a car"
}

super.describe() calls the base implementation. It works for properties too:

open class Base {
    open val name: String get() = "base"
}

class Derived : Base() {
    override val name: String get() = super.name + "-derived"
}

Overriding properties

open class Base {
    open val status: String = "new"
    open var count: Int = 0
}

class Derived : Base() {
    override val status = "active"
    override var count = 10
}

A val can be overridden by a var, because a var supplies the getter a val promises plus a setter. The reverse does not compile.

A property can also be overridden by a computed one:

class Computed : Base() {
    override val status: String get() = if (expired()) "expired" else "active"
}

That is the substitution the open val contract permits, the base promised a getter, not a stored field.

Two interfaces with the same default

interface A {
    fun greet() = "Hello from A"
}

interface B {
    fun greet() = "Hello from B"
}

class C : A, B {
    override fun greet(): String = super<A>.greet() + " and " + super<B>.greet()
}

A class inheriting the same signature from two interfaces must override it, the compiler refuses to choose. super<A> and super<B> disambiguate, and the qualified form is available only inside that override.

This is Kotlin’s version of the diamond problem, and the resolution is explicit rather than rule-based. A language that picked one silently would make the behaviour depend on declaration order.

Constructors and initialisation order

open class Base(val id: Long) {
    init { println("Base init") }
    open val label: String = "base"
}

class Derived(id: Long) : Base(id) {
    init { println("Derived init") }
    override val label: String = "derived"
}

The base is constructed first: Base’s initialisers and init blocks run to completion before any of Derived’s. That produces the trap worth stating once per article on this topic:

open class Base {
    open val label: String = "base"
    init { println(label.length) }      // NullPointerException when constructing Derived
}

At that moment Derived.label has not been assigned, so an overridden non-nullable property is null, the compiler warns; it does not stop you. Pass the value as a constructor parameter instead of reading an open member during construction.

Abstract, sealed and interface

Four ways to define a supertype, and the choice is about who may extend it:

extendable bystatewhen
open classanyoneyesa concrete base with a working default
abstract classanyoneyesa base that is incomplete on its own
sealed classsame module onlyyesa closed set of variants
interfaceanyoneno backing fieldsa capability, and a class can have many

sealed is the one worth reaching for more often than people do. It makes a when exhaustive, so adding a variant turns every incomplete when into a compile error rather than a silent default branch — see abstract classes for the comparison in full.

Default to an interface. Kotlin interfaces carry default implementations, which removes the historic reason to prefer an abstract class; what is left is stored state and a constructor.

Visibility across the hierarchy

open class Base {
    private val hidden = 1          // not visible to subclasses
    protected val shared = 2        // visible to subclasses only
    internal val moduleWide = 3     // visible across the module
    open val public = 4
}

protected in Kotlin means “this class and its subclasses” and — unlike Java — not the same package. That is a genuine tightening: Java’s protected leaks a member to every class in the package, including ones that are not subclasses.

internal has no Java equivalent. It restricts visibility to the compilation module, which is what makes sealed useful and what lets a library expose something to its own code without exposing it to consumers. In the bytecode an internal member is public with a mangled name, so a determined Java caller can still reach it — it is a compile-time boundary, not a security one.

An override cannot narrow visibility: a protected member cannot become private in a subclass, because that would break substitutability. Widening is allowed.

Prefer composition

class Car(private val engine: Engine) {
    fun start() = engine.start()
}

Inheritance couples a subclass to its parent’s implementation, not just its interface — an override that calls super depends on what the base does today. Composition delegates instead, and Kotlin has first-class support for it:

interface Engine {
    fun start(): String
}

class PetrolEngine : Engine {
    override fun start() = "vroom"
}

class Car(engine: Engine) : Engine by engine     // every Engine method delegated

by generates the forwarding methods, so Car satisfies Engine without a hierarchy and without boilerplate. Overriding one of them is a matter of declaring it.

One caveat: the delegate is captured at construction, so a method the delegating class overrides is not seen by the delegate’s own internal calls. Delegation is not inheritance, and that asymmetry is the practical difference between them.

More Kotlin in the Kotlin guides, including classes and constructors.

Frequently asked questions

Why can’t I extend my Kotlin class?

Classes are final by default. Mark it open — and each member you intend to be overridable, since opening the class does not open its members.

Is override optional like Java’s @Override?

No, it is mandatory. A member that does not actually override anything is a compile error rather than a new method with a similar name.

Can an overriding method be overridden again?

Yes — an override member is implicitly open. Use final override to stop the chain.

How do I call the parent implementation?

super.method(), and the same for a property getter.

Can I override a val with a var?

Yes. A var provides the getter plus a setter. The reverse does not compile.

What happens if two interfaces provide the same default method?

The class must override it and disambiguate with super<A> and super<B>, the compiler will not choose for you.

Why is my overridden property null in the base constructor?

The base is fully initialised before the subclass’s initialisers run. Do not read open members during construction.

When should I use sealed instead of open?

When the set of subtypes is fixed and known in the module. It makes a when exhaustive, so a new variant becomes a compile error.

Should I use an abstract class or an interface?

Interface by default — Kotlin interfaces support default implementations. Abstract class when you need stored state or a constructor.

What does by do in a class header?

It delegates every member of that interface to the given object. The delegate does not see the delegating class’s overrides, which is the main way delegation differs from inheritance.