Inheritance & Interfaces

Kotlin Fundamentals — Inheritance & Interfaces
Kotlin Fundamentals
Course 1 · Chapter 5 · Inheritance & Interfaces

🧬 Inheritance & Interfaces

Kotlin classes are closed to inheritance by default — the opposite of Java's default. This chapter covers how to deliberately open a class up for subclassing, abstract classes, interfaces, polymorphism, and sealed classes, which give Kotlin a safer alternative to open-ended inheritance hierarchies for a common category of problem.

🔒 Classes Are final by Default

class Animal(val name: String) // class Dog : Animal("Rex") // Compile error: Animal is final

In Java, every class can be subclassed unless it's explicitly marked final. Kotlin inverts that: every class is implicitly final unless explicitly marked open. This is a deliberate design choice — inheritance is powerful but easy to misuse, so Kotlin makes "can this be subclassed?" an explicit decision rather than an accident.

open class Animal(val name: String) { open fun makeSound() = "..." } class Dog(name: String) : Animal(name) { override fun makeSound() = "Woof!" } val pet: Animal = Dog("Rex") println(pet.makeSound()) // Woof! — polymorphism: Animal reference, Dog behavior

Both the class (open class Animal) and the specific function being overridden (open fun makeSound()) need the open keyword — a class being open doesn't automatically make its members overridable. And the subclass must use override explicitly; Kotlin never lets an override happen silently.

Inheritance Defaults: Java vs Kotlin

JavaKotlin
DefaultOpen (subclassable)Final (not subclassable)
Opt in to closingfinal class Fooclass Foo (already final)
Opt in to openingclass Foo (already open)open class Foo
Marking an override@Override (annotation, optional)override (keyword, required)

🧩 Abstract Classes

An abstract class can't be instantiated directly, and can declare members with no implementation, which subclasses are forced to provide:

abstract class Shape { abstract fun area(): Double fun describe() = "This shape has an area of ${area()}" } class Circle(val radius: Double) : Shape() { override fun area() = Math.PI * radius * radius } // val s = Shape() // Compile error: Shape is abstract, cannot be instantiated println(Circle(2.0).describe()) // This shape has an area of 12.566...

Note that abstract members don't need the open keyword — being abstract already implies they must be overridden. describe() shows a common pattern: a concrete method in the abstract class that relies on the abstract method being implemented by whatever subclass eventually exists.

🔌 Interfaces

A Kotlin interface can declare abstract members (no body) and also provide default implementations, unlike a pre-Java-8 interface:

interface Drivable { val maxSpeed: Int // abstract property — implementer must provide it fun drive() // abstract method fun honk() = println("Beep!") // default implementation — free for implementers } class Car(override val maxSpeed: Int) : Drivable { override fun drive() = println("Driving at up to $maxSpeed km/h") } val car = Car(180) car.drive() // Driving at up to 180 km/h car.honk() // Beep! — used as-is, never overridden

Multiple Interfaces, One Class

A class can implement any number of interfaces (class Car : Drivable, Insurable), unlike class inheritance, where Kotlin — like Java — allows only one superclass. Interfaces are how Kotlin achieves "multiple inheritance" of behavior.

Interface vs Abstract Class

Use an interface for a capability shared across unrelated classes (Drivable, Comparable). Use an abstract class when subclasses share actual state (constructor properties) and a genuine "is-a" relationship, not just a shared capability.

🔐 Sealed Classes

A sealed class restricts its subclasses to a known, fixed set, all declared in the same file. This is Kotlin's answer to representing "one of these specific things, and nothing else":

sealed class NetworkResult data class Success(val data: String) : NetworkResult() data class Error(val message: String) : NetworkResult() object Loading : NetworkResult() fun handle(result: NetworkResult) = when (result) { is Success -> println("Got: ${result.data}") is Error -> println("Failed: ${result.message}") Loading -> println("Loading...") // No "else" branch needed — the compiler knows these three are the only possibilities }
⚠ The Real Payoff: Exhaustiveness Checking

Because a sealed class's subclasses are fixed and known, a when expression that covers every branch doesn't need (or allow, in expression form) an else. If a new subclass of NetworkResult is added later and this when isn't updated, the compiler flags it as an error — not a bug discovered at runtime. A plain interface or open class gives no such guarantee, since anyone, anywhere, could add a new implementation.

💻 Coding Challenges

Challenge 1: Open Class & Override

Write an open class Employee(val name: String) with an open fun payDescription() returning a generic string. Create a subclass Manager(name: String, val bonus: Double) : Employee(name) that overrides payDescription() to mention the bonus. Store a Manager in an Employee-typed variable and call payDescription() on it.

Goal: Practice open/override and see polymorphism through a superclass-typed reference.

→ Solution

Challenge 2: Interface with a Default Method

Write an interface Greetable with an abstract property name: String, an abstract function customGreeting(): String, and a default function greet() that prints customGreeting(). Implement it in two different classes with different greetings, and call greet() on each.

Goal: Combine abstract members with a default implementation in one interface.

→ Solution

Challenge 3: Sealed Class with when

Write a sealed class Shape with three subclasses: data class Circle(val radius: Double), data class Square(val side: Double), and data class Rectangle(val width: Double, val height: Double). Write a function area(shape: Shape): Double using a when expression with no else branch that computes the correct area for each.

Goal: Practice sealed classes and see exhaustive when in action.

→ Solution

💡 Sealed Classes Preview Android's UI-State Pattern

The NetworkResult example above (Success / Error / Loading) is close to a verbatim preview of how UI state is modeled in real Android apps with Jetpack Compose — a screen's state is almost always a sealed class with exactly this shape. Getting comfortable with sealed classes and exhaustive when now sets up that pattern well ahead of time.

🎯 What's Next

Next chapter: Collections & Functional Operators — List/Set/Map, and the map/filter/reduce/flatMap operators used constantly in idiomatic Kotlin.