Inheritance and open Classes
Inheritance lets one class reuse and extend the behavior of another: a Dog and a Cat can both build on a shared Animal, a Manager can build on a shared Employee. Kotlin supports single-class inheritance just like Java, but it flips Java’s default: every class and every member is final (non-inheritable, non-overridable) unless you explicitly mark it open. This one design decision quietly prevents a huge class of fragile-base-class bugs, and understanding it is the key to understanding Kotlin inheritance.
Overview / How It Works
In Java, any class can be subclassed and any method can be overridden unless you go out of your way to add final. Kotlin’s designers considered this backwards: an unplanned-for override is a common source of bugs, because a subclass can break assumptions the base class author never anticipated. So Kotlin classes are final by default. To allow a class to be subclassed, you must mark it open class. To allow a member (a function or a property) inside an open class to be overridden, you must separately mark that member open too. Marking the class open does not automatically open its members — both the class and the specific member need the modifier.
When a subclass wants to override an open member, it uses the override keyword. This is mandatory in Kotlin (unlike Java’s optional @Override annotation) — the compiler checks that you are genuinely overriding something with a matching signature, catching typos and signature mismatches that would otherwise silently create an unrelated new method. An overriding member is itself open for further overriding by default, so a grandchild class can override it again; if you want to stop that, mark the override final override fun ....
Kotlin inheritance uses a colon in place of Java’s extends: class Dog(name: String) : Animal(name). Notice the parentheses after Animal — a subclass must call one of the base class’s constructors as part of declaring the inheritance relationship (unless the subclass has no primary constructor, in which case each secondary constructor calls super(...) explicitly). This is different from Java, where the super(...) call is a statement hidden inside the constructor body; in Kotlin it is part of the class header itself, which makes the constructor chain easy to see at a glance.
A class can also inherit from an abstract class (declared with the abstract keyword). Abstract classes are implicitly open — you never write open abstract class — and they may declare abstract members (a function or property with no body/initializer) that every concrete subclass must implement, alongside regular open or non-open members with real implementations. Abstract classes cannot be instantiated directly; they exist purely to be extended.
Syntax
The general shape of an inheritance relationship looks like this:
open class Base(constructorParams) {
open fun member() { }
open val property: Type = value
}
class Derived(constructorParams) : Base(argumentsPassedToBase) {
override fun member() { }
override val property: Type = newValue
}
| Modifier | Meaning |
|---|---|
open class |
This class may be subclassed. Without it, the class is final. |
open fun / open val/var |
This member may be overridden in a subclass. Without it, the member is final even inside an open class. |
override |
Required on a subclass member that replaces an open member from the superclass. Enforced by the compiler. |
abstract class |
Implicitly open; may contain members with no implementation that subclasses must supply. |
abstract fun / abstract val |
No body/initializer in the base class; every concrete subclass must override it. |
final override |
Overrides a member but forbids any further overriding by classes below it. |
super |
Refers to the superclass implementation, e.g. super.member() to call the base version from an override. |
Examples
Example 1: Basic overriding and polymorphism
open class Animal(val name: String) {
open fun makeSound(): String {
return "Some generic sound"
}
}
class Dog(name: String) : Animal(name) {
override fun makeSound(): String {
return "Woof"
}
}
class Cat(name: String) : Animal(name) {
override fun makeSound(): String {
return "Meow"
}
}
fun main() {
val animals: List<Animal> = listOf(Dog("Rex"), Cat("Whiskers"))
for (animal in animals) {
println("${animal.name} says ${animal.makeSound()}")
}
}
Output:
Rex says Woof
Whiskers says Meow
Both Dog and Cat pass their constructor parameter straight through to Animal‘s constructor via Animal(name) in the class header. Even though the loop variable animal is statically typed as Animal, calling makeSound() dispatches to whichever override actually matches the runtime object — this is dynamic dispatch, the essence of polymorphism, and it only works here because makeSound was declared open.
Example 2: Abstract classes, overriding a property, and super
abstract class Shape {
abstract val area: Double
open fun describe(): String {
return "A shape with area ${"%.2f".format(area)}"
}
}
class Circle(val radius: Double) : Shape() {
override val area: Double
get() = Math.PI * radius * radius
override fun describe(): String {
return "Circle: ${super.describe()}"
}
}
class Rectangle(val width: Double, val height: Double) : Shape() {
override val area: Double
get() = width * height
}
fun main() {
val shapes: List<Shape> = listOf(Circle(2.0), Rectangle(3.0, 4.0))
for (shape in shapes) {
println(shape.describe())
}
}
Output:
Circle: A shape with area 12.57
A shape with area 12.00
Shape is abstract, so it can declare area with no initializer — each subclass must supply one. Circle overrides area with a custom getter that computes it from radius on every access, and it also overrides describe(), calling super.describe() to reuse the base formatting logic instead of duplicating it. Rectangle only overrides the required area and simply inherits describe() unchanged, showing that overriding is always optional for non-abstract open members.
Example 3: A more realistic hierarchy with protected state
open class Employee(val name: String, protected val baseSalary: Double) {
open fun bonus(): Double = baseSalary * 0.05
fun totalPay(): Double = baseSalary + bonus()
}
class Manager(name: String, baseSalary: Double, val teamSize: Int) : Employee(name, baseSalary) {
override fun bonus(): Double = super.bonus() + teamSize * 100.0
}
fun main() {
val employee = Employee("Ana", 50000.0)
val manager = Manager("Ravi", 60000.0, 5)
println("${employee.name}: ${employee.totalPay()}")
println("${manager.name}: ${manager.totalPay()}")
}
Output:
Ana: 52500.0
Ravi: 63500.0
baseSalary is protected, so it is visible to Employee itself and to subclasses like Manager, but not from outside code such as main. Manager overrides bonus() and calls super.bonus() to build on the base calculation rather than reimplementing the 5% rule, then adds a per-team-member bonus on top. totalPay() is not open, so it behaves identically for every subclass and always calls whichever bonus() override actually applies.
How It Works Step by Step
When you write class Manager(...) : Employee(...), constructing a Manager proceeds in a fixed order: first, the arguments to Employee(...) in the class header are evaluated; second, Employee‘s own primary constructor and property initializers and init blocks run top to bottom, fully completing the base object’s setup; only after that does control return to Manager, which runs its own property initializers and init blocks in declaration order. The subclass is never partially base-constructed and partially derived-constructed at the same time from the outside — the base always finishes first.
Method and property dispatch, however, is virtual throughout this process: if the base class constructor calls an open function, and the object being constructed is actually a subclass instance, the subclass’s override runs — even though the subclass’s own initializers have not executed yet. This is exactly why calling open members from a base class constructor is dangerous, covered in the next section.
Common Mistakes
Mistake 1: Forgetting that members need their own open
Marking the class open is not enough; each member you intend to override needs its own open modifier.
open class Base {
fun greet() {
println("Hi from Base")
}
}
class Derived : Base() {
override fun greet() {
println("Hi from Derived")
}
}
This fails to compile with “‘greet’ in ‘Base’ is final and cannot be overridden”, because greet was never marked open. Fix it by opening the specific member:
open class Base {
open fun greet() {
println("Hi from Base")
}
}
class Derived : Base() {
override fun greet() {
println("Hi from Derived")
}
}
Mistake 2: Calling an open member from a base class constructor
Because dispatch is virtual during construction but subclass initializers run last, reading an overridden property (or calling an overridden function that reads one) from the base constructor can observe uninitialized state.
open class Base {
open val greeting: String = "Hello from Base"
init {
printGreeting()
}
open fun printGreeting() {
println(greeting)
}
}
class Derived : Base() {
override val greeting: String = "Hello from Derived"
}
fun main() {
Derived()
}
The output is null, not Hello from Derived. During Base‘s init block, printGreeting() dispatches virtually to the inherited implementation, which reads the overridden greeting — but Derived‘s property initializer has not run yet, so the backing field is still at its default (null, despite the declared type being the non-null String). The fix is to avoid depending on subclass state from the base constructor entirely, for example by passing the value in through the constructor instead of overriding a property:
open class Base(private val greeting: String) {
init {
println(greeting)
}
}
class Derived : Base("Hello from Derived")
fun main() {
Derived()
}
This now prints Hello from Derived, because a constructor argument is fully available before the init block runs — there is no virtual dispatch involved.
Mistake 3: Expecting silent field-hiding like Java
In Java, redeclaring a field with the same name in a subclass silently hides the parent’s field without warning. Kotlin refuses to do this quietly.
open class Vehicle {
open val wheels: Int = 4
}
class Motorcycle : Vehicle() {
val wheels: Int = 2
}
This fails to compile with “Property ‘wheels’ hides member of supertype ‘Vehicle’ and needs ‘override’ modifier”. Kotlin forces you to be explicit about whether you mean to override or to deliberately shadow (which requires acknowledging it). The fix is simply to add override:
open class Vehicle {
open val wheels: Int = 4
}
class Motorcycle : Vehicle() {
override val wheels: Int = 2
}
fun main() {
val vehicle: Vehicle = Motorcycle()
println("Wheels: ${vehicle.wheels}")
}
Output:
Wheels: 2
Best Practices
- Keep classes final (the default) unless you have a concrete reason to allow subclassing; open classes are part of your public API contract and harder to change later.
- Prefer composition over inheritance when the relationship isn’t a genuine “is-a” relationship — inheritance couples the subclass tightly to the base class’s implementation details.
- Never call an open function or read an open property from an
initblock or primary constructor body; pass any subclass-dependent values in through the constructor instead. - Use
abstract classwhen you have shared state or partial implementation to provide alongside members that must be filled in; use an interface when you only need a contract with no shared state. - Mark an override
final overridewhen you specifically want to stop further overriding down the hierarchy. - Favor shallow hierarchies. Kotlin encourages sealed classes, interfaces, and extension functions as alternatives to deep inheritance trees.
Practice Exercises
- Write an open class
Instrumentwith an open functionplay(): String, then createGuitarandPianosubclasses that override it. Put instances of both in aList<Instrument>and print each one’splay()result in a loop. - Write an abstract class
PaymentMethodwith an abstract functionprocess(amount: Double): String. ImplementCreditCardandBankTransfersubclasses with different processing messages, then callprocesspolymorphically on a list of both. - Create an open class
Account(open val balance: Double)and a subclassSavingsAccountthat overridesbalanceby adding accumulated interest. Predict, then verify, what happens if you try to readbalancefrom aninitblock inAccountbefore understanding construction order — explain why it’s risky even if it happens to work in your specific case.
Summary
- Kotlin classes and members are final by default; use
open classandopen fun/open val/varto explicitly allow subclassing and overriding. - The
overridekeyword is mandatory and compiler-checked, preventing silent mismatches or accidental field-hiding that Java allows. - A subclass calls a base constructor directly in its class header:
class Derived(...) : Base(...). abstract classis implicitly open and can declare members with no implementation that every concrete subclass must supply.super.member()lets an override call and build on the base implementation instead of replacing it entirely.- Base class constructors run to completion before subclass initializers run, but virtual dispatch is still active during that window — never call an open member from a base constructor if it depends on subclass state.
