Skip to main content

Kotlin Inheritance Constraints

Introduction

In object-oriented programming, inheritance is a powerful feature that allows classes to inherit properties and behaviors from other classes. However, Kotlin places certain constraints on inheritance to make code more predictable and safer. Understanding these constraints is essential for writing robust Kotlin code.

In this tutorial, we'll explore the various constraints Kotlin imposes on inheritance, why they exist, and how to work with them effectively.

Kotlin's Default: Classes are Final

One of the most significant differences between Kotlin and many other object-oriented languages like Java is that classes in Kotlin are final by default. This means they cannot be inherited from unless explicitly allowed.

kotlin
class RegularClass {
fun sayHello() = "Hello"
}

// This will cause a compilation error
class ChildClass : RegularClass() {
// Error: This type is final, so it cannot be inherited from
}

Why Final by Default?

This design choice was made to address the Fragile Base Class Problem, where changes to a base class can unexpectedly break subclasses. By making classes final by default, Kotlin encourages developers to design their class hierarchies more carefully.

The open Keyword

To allow a class to be inherited from, you must explicitly mark it with the open keyword:

kotlin
open class OpenClass {
fun sayHello() = "Hello"
}

// Now inheritance is allowed
class ChildClass : OpenClass() {
// This works fine!
}

Member Inheritance Constraints

In Kotlin, the inheritance constraints apply not only to classes but also to their members (properties and functions).

Methods are Final by Default

Just like classes, methods are also final by default and cannot be overridden:

kotlin
open class Parent {
fun normalMethod() = "I cannot be overridden"
open fun openMethod() = "I can be overridden"
}

class Child : Parent() {
// This would cause a compilation error
// fun normalMethod() = "Trying to override"

// This is allowed because the method is marked as open
override fun openMethod() = "Overridden successfully"
}

The override Keyword

In Kotlin, when you override a method or property from a superclass, you must use the override keyword. This makes it explicit that you are intentionally overriding a member from the parent class:

kotlin
open class Animal {
open fun makeSound() = "..."
}

class Dog : Animal() {
override fun makeSound() = "Woof!"
}

class Cat : Animal() {
override fun makeSound() = "Meow!"
}

fun main() {
val dog = Dog()
val cat = Cat()

println(dog.makeSound()) // Output: Woof!
println(cat.makeSound()) // Output: Meow!
}

Abstract Classes and Members

Abstract classes in Kotlin follow a similar pattern but with some special characteristics:

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

// Non-abstract function in an abstract class
// Note that you don't need to mark it as 'open' - it already is
fun description() = "This is a shape"
}

class Circle(private val radius: Double) : Shape() {
override fun area() = Math.PI * radius * radius

// You can override non-abstract methods too
override fun description() = "This is a circle with radius $radius"
}

fun main() {
val circle = Circle(5.0)
println("Area: ${circle.area()}") // Output: Area: 78.53981633974483
println(circle.description()) // Output: This is a circle with radius 5.0
}

Key points about abstract classes:

  • They cannot be instantiated directly
  • They can have abstract members that must be implemented by subclasses
  • Non-abstract members in abstract classes are implicitly open (but can be marked final)

Preventing Overrides with final

If you have an open class but want to prevent specific methods from being overridden in subclasses, you can mark those methods as final:

kotlin
open class Vehicle {
open fun start() = "Vehicle started"
final fun stop() = "Vehicle stopped"
}

class Car : Vehicle() {
override fun start() = "Car started"

// This would cause a compilation error
// override fun stop() = "Car stopped"
}

Sealed Classes

Kotlin provides sealed classes, which represent restricted class hierarchies. A sealed class can be subclassed only by its nested classes or classes in the same file:

kotlin
sealed class Result {
class Success(val data: Any) : Result()
class Error(val message: String) : Result()
object Loading : Result()
}

fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("Success with data: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
is Result.Loading -> println("Loading...")
// No need for an 'else' clause as all possible subclasses are covered
}
}

fun main() {
val success = Result.Success("Data loaded")
val error = Result.Error("Connection failed")
val loading = Result.Loading

handleResult(success) // Output: Success with data: Data loaded
handleResult(error) // Output: Error: Connection failed
handleResult(loading) // Output: Loading...
}

Real-world Application: Building a UI Component System

Let's see how these inheritance constraints can be applied in a practical example - building a UI component system:

kotlin
// Base component class - open to allow inheritance
open class UIComponent(val id: String) {
open fun render(): String = "Generic component"

// This method is final and cannot be overridden
final fun uniqueId(): String = "component_$id"
}

// Abstract component that enforces certain behaviors
abstract class InputComponent(id: String) : UIComponent(id) {
abstract fun getValue(): Any

// All input components show a label
open fun renderLabel(label: String): String = "Label: $label"
}

// Concrete input implementation
class TextInput(id: String, private val value: String = "") : InputComponent(id) {
override fun render(): String = "Text input with value: $value"
override fun getValue(): Any = value
}

// Another concrete implementation
class Checkbox(id: String, private val checked: Boolean = false) : InputComponent(id) {
override fun render(): String = if (checked) "✅ Checked" else "⬜ Unchecked"
override fun getValue(): Any = checked

// Specialized label rendering for checkboxes
override fun renderLabel(label: String): String = "$label: [ ]"
}

fun main() {
val components = listOf(
TextInput("name", "John Doe"),
Checkbox("subscribe", true)
)

for (component in components) {
println("Component ID: ${component.uniqueId()}")
println("Rendered: ${component.render()}")
println("Value: ${component.getValue()}")
println("Label: ${component.renderLabel("Field")}")
println("---")
}
}

Output:

Component ID: component_name
Rendered: Text input with value: John Doe
Value: John Doe
Label: Label: Field
---
Component ID: component_subscribe
Rendered: ✅ Checked
Value: true
Label: Field: [ ]
---

This example demonstrates how to use various inheritance constraints to create a flexible yet controlled UI component system:

  • Base class is open to allow inheritance
  • final methods protect critical functionality
  • abstract classes and methods enforce implementation requirements
  • override keyword makes method overriding explicit

Summary

In this tutorial, we've explored Kotlin's inheritance constraints, which are designed to make code more predictable and robust:

  1. Classes are final by default and must be explicitly marked open to allow inheritance
  2. Methods are also final by default and must be marked open to allow overriding
  3. The override keyword is required when overriding a method
  4. Abstract classes can have abstract methods that must be implemented by subclasses
  5. The final keyword can prevent further overriding of methods
  6. Sealed classes restrict the inheritance hierarchy to specific classes

These constraints may seem restrictive at first, especially if you're coming from languages like Java, but they lead to more intentional and safer inheritance hierarchies, reducing common bugs and making code easier to maintain.

Exercises

  1. Create a Vehicle class hierarchy with appropriate inheritance constraints:

    • Make a base Vehicle class with open methods for startEngine() and stopEngine()
    • Create a final method getVehicleType() that cannot be overridden
    • Implement Car and Motorcycle subclasses that override the appropriate methods
  2. Implement a BankAccount hierarchy:

    • Create an abstract BankAccount class with abstract methods for deposit() and withdraw()
    • Implement CheckingAccount and SavingsAccount classes with appropriate implementations
    • Make sure the accountNumber property cannot be overridden in subclasses

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)