Kotlin Abstract Classes with Examples
Kotlin 13 min read
Abstract members are open without saying so, an abstract class can hold state where an interface cannot, and sealed is the third option that makes a when exhaustive.
An abstract class declares members without implementing them and cannot be instantiated. The Kotlin
specifics worth knowing are two exemptions from the language’s usual defaults: an abstract class is
implicitly open, and so is every abstract member, neither needs the keyword that a concrete class
requires.
Written against Kotlin 1.9.
Declaring one
abstract class Shape(val name: String) {
abstract fun area(): Double // no body, must be overridden
abstract val sides: Int // abstract property
open fun describe(): String = // concrete, overridable
"$name with $sides sides, area ${area()}"
fun report() = println(describe()) // concrete, final
}
class Circle(private val radius: Double) : Shape("Circle") {
override fun area() = Math.PI * radius * radius
override val sides = 0
override fun describe() = "Circle of radius $radius"
}
class Rectangle(private val w: Double, private val h: Double) : Shape("Rectangle") {
override fun area() = w * h
override val sides = 4
}
Three visibility rules are in play and they differ from Java.
Kotlin classes are final by default, so a concrete class must be marked open to be extended.
An abstract class is exempt. Extending it is the only thing it is for.
Kotlin members are final by default, so describe() needs open to be overridable and
report() is deliberately not. An abstract member is exempt for the same reason.
override is mandatory. Java’s @Override is advisory; Kotlin’s keyword is required, and
omitting it is a compile error rather than an accidental overload.
An overriding member is itself open unless marked final override, which is worth doing when a
subclass has finished the hierarchy.
Constructors run in a specific order
abstract class Base(val id: Long) {
init {
println("Base init, id=$id")
}
open val label: String = "base"
}
class Derived(id: Long) : Base(id) {
override val label: String = "derived"
init {
println("Derived init, label=$label")
}
}
The base class is fully constructed first: Base’s init runs before Derived’s property
initialisers. That produces the classic trap:
abstract class Base {
open val name: String = "base"
init {
println(name.length) // NullPointerException from a non-nullable property
}
}
class Derived : Base() {
override val name: String = "derived"
}
At the moment Base’s init runs, Derived.name has not been assigned, so the overridden property
is null despite being declared non-nullable. Never call an open member from a constructor or an
initialiser. If the base needs a value from the subclass, take it as a constructor parameter.
Abstract class or interface?
interface Drawable {
val strokeWidth: Int // no backing field allowed
fun draw()
fun describe() = "drawable" // default implementation is allowed
}
Kotlin interfaces can carry default implementations, which removes the historic reason to prefer an abstract class. What is left is genuine:
| abstract class | interface | |
|---|---|---|
| State (backing fields) | yes | no |
| Constructor | yes | no |
| How many can a class have | one | many |
| Non-public members | yes | only private |
| Constructor parameters | yes | no |
The deciding question is state. An interface property has no backing field — it is a getter the implementer must supply — so anything that needs to store something shared belongs in an abstract class.
The other deciding question is whether the type is a category or a capability. Shape is what
something is; Comparable is what it can do. A class has one of the first and many of the
second, which is what the single-inheritance limit encodes.
Default to an interface, and reach for an abstract class when shared state or a constructor is genuinely needed.
Abstract members can override concrete ones
The reverse direction is legal and occasionally useful:
abstract class Base {
open fun compute(): Int = 0
}
abstract class Middle : Base() {
abstract override fun compute(): Int // forces every concrete subclass to decide
}
Middle removes the inherited default and makes the member abstract again, so a subclass cannot
silently accept 0. It is a way of saying “the default was wrong for this branch of the hierarchy”.
Overriding a val with a var
abstract class Base {
abstract val status: String
}
class Derived : Base() {
override var status: String = "new" // val -> var is allowed
}
Widening val to var works because a var supplies everything a val promises plus a setter. The
reverse does not compile — a var in the base promises a setter that a val cannot provide.
sealed, the third option
sealed class Result {
data class Success(val value: String) : Result()
data class Failure(val error: Throwable) : Result()
data object Loading : Result()
}
A sealed class is abstract with a closed set of direct subclasses, all known at compile time. That
makes a when over it exhaustive:
fun render(result: Result): String = when (result) {
is Result.Success -> result.value
is Result.Failure -> "Error: ${result.error.message}"
Result.Loading -> "Loading…"
}
No else branch, and adding a fourth subclass makes every such when stop compiling — which is the
point. An abstract class with an else branch silently sends the new case down the default path.
sealed interface exists too and is usually the better choice, since a type can implement several.
Use sealed when the set of subtypes is fixed and known: a result, a state, an event, a parse
outcome. Use plain abstract when third parties are meant to extend it.
The template method, and its cost
The pattern abstract classes exist for is a fixed algorithm with variable steps:
abstract class Importer {
fun run(path: Path) { // final — the algorithm does not vary
val raw = read(path)
val records = parse(raw)
validate(records)
persist(records)
}
protected abstract fun parse(raw: String): List<Record>
protected abstract fun persist(records: List<Record>)
protected open fun validate(records: List<Record>) { } // optional hook
private fun read(path: Path) = Files.readString(path)
}
run is final on purpose: a subclass supplies steps, not a new order. validate is open with an
empty body, so overriding it is optional. Marking the abstract members protected keeps them out of
the public API — they are extension points, not operations a caller invokes.
The cost is that the base class now dictates control flow, and a subclass that needs a different order has to fight it or duplicate it. The composition alternative passes the varying steps in:
class Importer(
private val parse: (String) -> List<Record>,
private val persist: (List<Record>) -> Unit,
)
No hierarchy, each step testable alone, and the shared algorithm still in one place. Worth reaching for first; the abstract class earns its place when the steps share state or there are enough of them that a constructor of lambdas becomes unreadable.
Instantiating with an object expression
An abstract class cannot be instantiated, but an anonymous subclass can:
val square = object : Shape("Square") {
override fun area() = 4.0
override val sides = 4
}
That is Kotlin’s anonymous class. It is useful for a one-off in a test or a callback, and it captures the enclosing scope, so a long-lived one can hold a reference longer than intended.
More Kotlin in the Kotlin guides, including inheritance and overriding and classes and constructors.
Frequently asked questions
Does an abstract class need the open keyword?
No. abstract implies open for both the class and
its abstract members. A concrete member still needs open to be overridable.
Why is override mandatory in Kotlin?
To make overriding deliberate. Without the keyword the compiler rejects the member rather than treating it as a new one, which removes a whole class of accidental-overload bugs.
Can an abstract class have a constructor?
Yes, including parameters, and it runs before the subclass’s initialisers. That is one of the main differences from an interface.
Why is my overridden property null inside the base class’s init block?
The subclass’s initialisers have not run yet. Never call an open member from a constructor; pass the value as a constructor parameter instead.
Abstract class or interface?
Interface by default — Kotlin interfaces support default implementations. Use an abstract class when you need stored state, a constructor, or non-public members.
Can an interface have properties?
It can declare them, but with no backing field — the implementer supplies the getter. Anything that must store a value needs an abstract class.
Can I override a val with a var?
Yes. A var provides the getter a val promises plus a setter.
The reverse does not compile.
Can an abstract member override a concrete one?
Yes — abstract override fun f() removes an
inherited implementation and forces subclasses to supply their own.
When should I use sealed instead?
When the set of direct subclasses is fixed and known at compile
time. It makes a when exhaustive, so adding a subtype turns every incomplete when into a compile
error.
How do I instantiate an abstract class for a test?
An object expression — object : Shape("x") { … }
creates an anonymous subclass inline.