Skip to main content

Kotlin Open Classes

In this lesson, we'll dive into one of Kotlin's unique features - the concept of "open classes." Understanding this concept is essential for working with inheritance in Kotlin, and it highlights one of the key philosophical differences between Kotlin and other languages like Java.

Introduction to Open Classes

In many object-oriented programming languages like Java, classes are inheritable by default. This means you can extend any class unless it's explicitly marked as final. Kotlin takes the opposite approach – all classes in Kotlin are final by default and cannot be inherited from unless explicitly marked as open.

This "closed by default" design was a deliberate choice by Kotlin's creators to promote:

  • More thoughtful class design
  • Better code organization
  • Safer inheritance hierarchies
  • Prevention of the "fragile base class" problem

Basic Syntax for Open Classes

To make a class inheritable in Kotlin, we use the open keyword before the class keyword:

kotlin
open class Animal {
// Class implementation
}

// Now we can inherit from Animal
class Dog : Animal() {
// Dog implementation
}

If you try to inherit from a class that isn't marked as open, you'll get a compilation error:

kotlin
class Vehicle {
// Class implementation
}

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

Open Methods and Properties

Like classes, methods and properties in Kotlin are also final by default. If you want to allow overriding of methods or properties in subclasses, you need to mark them as open too:

kotlin
open class Animal {
open fun makeSound() {
println("Some generic animal sound")
}

fun eat() {
println("The animal is eating")
}
}

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

// This would cause a compilation error:
// override fun eat() { ... }
// Error: 'eat' in 'Animal' is final and cannot be overridden
}

Let's see the output:

val animal = Animal()
animal.makeSound() // Output: Some generic animal sound

val dog = Dog()
dog.makeSound() // Output: Woof!
dog.eat() // Output: The animal is eating

Properties and Open Classes

Properties can also be marked as open to allow overriding:

kotlin
open class Animal {
open val sound: String = "Generic sound"
val type: String = "Generic animal"
}

class Cat : Animal() {
override val sound: String = "Meow"
// type cannot be overridden because it's not marked as open
}

Usage:

kotlin
val animal = Animal()
println("Animal makes: ${animal.sound}") // Output: Animal makes: Generic sound

val cat = Cat()
println("Cat makes: ${cat.sound}") // Output: Cat makes: Meow
println("Cat type: ${cat.type}") // Output: Cat type: Generic animal

Preventing Further Inheritance with Final

Sometimes you might want to allow a class to be inherited but prevent specific subclasses from being further extended. Kotlin lets you mark classes and members with the final keyword (though it's redundant for classes and members that are already final by default):

kotlin
open class Animal {
open fun makeSound() {
println("Some animal sound")
}
}

open class Dog : Animal() {
final override fun makeSound() {
println("Woof!")
}
}

class Poodle : Dog() {
// This would cause a compilation error:
// override fun makeSound() { ... }
// Error: 'makeSound' in 'Dog' is final and cannot be overridden
}

Real-World Examples

Example 1: UI Component Hierarchy

In Android development with Kotlin, UI components often use inheritance. Here's a simplified example:

kotlin
open class View {
open fun draw() {
println("Drawing a basic view")
}

open fun handleClick() {
println("View clicked")
}
}

open class Button : View() {
override fun draw() {
println("Drawing a button")
}

override fun handleClick() {
println("Button clicked")
}

open fun setText(text: String) {
println("Setting button text to: $text")
}
}

class ImageButton : Button() {
override fun draw() {
println("Drawing a button with an image")
}

override fun setText(text: String) {
println("Setting image button text to: $text")
}
}

Example 2: Game Character Hierarchy

kotlin
open class GameCharacter(val name: String) {
open var healthPoints = 100

open fun attack(): Int {
println("$name attacks!")
return 10
}

open fun takeDamage(damage: Int) {
healthPoints -= damage
println("$name took $damage damage. Health: $healthPoints")
}
}

open class Warrior(name: String) : GameCharacter(name) {
override var healthPoints = 150

override fun attack(): Int {
println("$name swings a sword!")
return 20
}
}

class Archer(name: String) : GameCharacter(name) {
override var healthPoints = 80

override fun attack(): Int {
println("$name fires an arrow!")
return 15
}

fun specialAttack(): Int {
println("$name fires three arrows at once!")
return 35
}
}

Usage example:

kotlin
fun main() {
val warrior = Warrior("Aragorn")
val archer = Archer("Legolas")

// Combat simulation
val warriorDamage = warrior.attack()
archer.takeDamage(warriorDamage)

val archerDamage = archer.specialAttack()
warrior.takeDamage(archerDamage)
}

Output:

Aragorn swings a sword!
Legolas took 20 damage. Health: 60
Legolas fires three arrows at once!
Aragorn took 35 damage. Health: 115

Open Classes vs. Abstract Classes vs. Interfaces

It's important to understand how open classes compare with other inheritance mechanisms in Kotlin:

FeatureOpen ClassAbstract ClassInterface
Can be instantiatedYesNoNo
Can contain implementationYesYesYes (default implementations)
Required to implement all methodsNoYes (non-abstract methods)Yes (unless default provided)
Can have state (properties with backing fields)YesYesNo (only abstract or with custom getters)

Best Practices for Open Classes

  1. Be conservative with the open modifier: Only mark classes as open when you specifically intend them to be inherited from.

  2. Document inheritance behavior: When creating an open class, document how it's meant to be extended.

  3. Consider alternatives first: Sometimes composition is better than inheritance, or an interface might be more appropriate.

  4. Test with subclasses: When you create an open class, test it with actual subclasses to ensure it works as expected when extended.

  5. Beware of exposing mutable state: If your open class has mutable state, consider how subclasses might affect it.

Summary

Kotlin's approach of "final by default" with explicit open modifiers represents a philosophical shift from languages like Java. By requiring developers to explicitly mark classes and members as inheritable, Kotlin encourages more thoughtful API design and helps prevent common inheritance-related issues.

Key points to remember:

  • Classes, methods, and properties in Kotlin are final by default
  • Use the open modifier to allow inheritance
  • Methods and properties in an open class still need to be marked as open to be overridable
  • You can use final to prevent further overriding in subclasses
  • Consider whether inheritance is really the best solution for your design problem

Exercises

  1. Create an open class Shape with open properties for name and color, and an open method calculateArea(). Then create subclasses for different shapes like Circle and Rectangle.

  2. Design a simple logging system with an open base class Logger and different implementation classes like ConsoleLogger and FileLogger.

  3. Create a class hierarchy for a simple banking system with an open Account class and subclasses like SavingsAccount and CheckingAccount.

  4. Take an existing class hierarchy from a project and analyze whether each class truly needs to be open. Try to refactor any unnecessary inheritance into composition.

Additional Resources



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