Abstract Classes

An abstract class is a class that cannot be instantiated on its own and is meant to be a base for other classes. It can mix fully-implemented (concrete) members with abstract members that have no body and must be filled in by every subclass. This makes abstract classes ideal when several related classes share some state and behavior but each needs to provide its own version of one or more operations.

Overview / How it works

In Kotlin, you declare an abstract class with the abstract keyword. Inside it, any member marked abstract has no implementation — only a signature — and any concrete subclass is required to override it. Members without the abstract keyword behave like normal class members: they have a body and are inherited as-is unless marked open (in which case a subclass may override them too).

The compiler enforces two things at compile time. First, you can never write ClassName() to create an instance of an abstract class directly — doing so is a compile error, because an abstract class may have unfinished members with no valid behavior to run. Second, any class that extends an abstract class must either implement every abstract member it inherits, or itself be declared abstract and defer the obligation further down the hierarchy. This is stricter and safer than a design based purely on inheritance with optional overrides: the compiler, not a runtime crash, catches an incomplete implementation.

A key detail: abstract members are implicitly open. You never write abstract open funabstract already implies it can (in fact, must) be overridden, so adding open is redundant. Abstract classes can also have constructors, constructor parameters, backing fields, and initializer blocks — something plain interfaces cannot do (interfaces cannot hold constructor state). This is the main practical reason to reach for an abstract class instead of an interface: you need shared, initialized state (like a name or a counter) alongside the shared contract.

An abstract class can also implement interfaces, extend another abstract class, and freely mix abstract and concrete members. A subclass of an abstract class is not required to be abstract itself — once every abstract member has a real implementation somewhere in the chain, the class becomes instantiable.

Syntax

abstract class ClassName(constructorParams) {
    abstract val someProperty: Type
    abstract fun someMethod(params): ReturnType

    open fun overridableMethod() { /* has a default body */ }
    fun finalMethod() { /* cannot be overridden */ }
}

class Subclass(constructorParams) : ClassName(constructorParams) {
    override val someProperty: Type = ...
    override fun someMethod(params): ReturnType { ... }
}
Part Meaning
abstract class Declares a class that cannot be instantiated directly.
abstract val/var A property with no initializer; every concrete subclass must supply one (as a property or a backing field).
abstract fun A function signature with no body; every concrete subclass must override it.
open fun A normal function with a body that subclasses may override.
(no modifier) fun A final function; subclasses inherit it as-is and cannot override it.
: ClassName(args) How a subclass extends the abstract class and forwards constructor arguments.

Examples

Example 1: A shared area calculation

abstract class Shape(val name: String) {
    abstract fun area(): Double

    fun describe(): String {
        return "$name has an area of ${"%.2f".format(area())}"
    }
}

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

class Rectangle(val width: Double, val height: Double) : Shape("Rectangle") {
    override fun area(): Double = width * height
}

fun main() {
    val shapes = listOf(Circle(3.0), Rectangle(4.0, 5.0))
    for (shape in shapes) {
        println(shape.describe())
    }
}

Output:

Circle has an area of 28.27
Rectangle has an area of 20.00

The abstract area() method has no shared formula — each shape computes it differently — but the concrete describe() method is written once in the base class and reused by every subclass. Note that name is a regular constructor property, something an interface could not hold.

Example 2: Abstract properties and a default that can be overridden

abstract class Employee(val name: String) {
    abstract val baseSalary: Double
    abstract fun calculateBonus(): Double

    open fun totalPay(): Double = baseSalary + calculateBonus()

    fun printPaySlip() {
        println("$name earns ${"%.2f".format(totalPay())}")
    }
}

class Developer(name: String, override val baseSalary: Double, private val linesShipped: Int) : Employee(name) {
    override fun calculateBonus(): Double = linesShipped * 0.5
}

class Manager(name: String, override val baseSalary: Double, private val teamSize: Int) : Employee(name) {
    override fun calculateBonus(): Double = teamSize * 200.0

    override fun totalPay(): Double = baseSalary + calculateBonus() + 500.0
}

fun main() {
    val staff: List = listOf(
        Developer("Ana", 60000.0, 400),
        Manager("Ben", 80000.0, 5)
    )
    for (person in staff) {
        person.printPaySlip()
    }
}

Output:

Ana earns 60200.00
Ben earns 81500.00

baseSalary is an abstract property, so each subclass supplies it via override val in its own primary constructor. totalPay() is open with a default formula; Developer keeps that default, while Manager overrides it to add a flat bonus. This is the difference between abstract (no default, mandatory override) and open (has a default, optional override).

Example 3: The template method pattern

abstract class DataProcessor {
    fun process() {
        val raw = readData()
        val clean = transformData(raw)
        saveData(clean)
    }

    protected abstract fun readData(): List
    protected abstract fun transformData(data: List): List
    protected abstract fun saveData(data: List)
}

class DoublingProcessor : DataProcessor() {
    override fun readData(): List {
        println("Reading raw numbers")
        return listOf(1, 2, 3)
    }

    override fun transformData(data: List): List {
        println("Doubling each number")
        return data.map { it * 2 }
    }

    override fun saveData(data: List) {
        println("Saved result: $data")
    }
}

fun main() {
    val processor: DataProcessor = DoublingProcessor()
    processor.process()
}

Output:

Reading raw numbers
Doubling each number
Saved result: [2, 4, 6]

process() is a final (non-abstract, non-open) method that fixes the overall algorithm’s shape, while the three steps it calls are abstract and supplied by the subclass. This is the template method pattern: the base class owns the sequence, subclasses own the details.

How it works step by step

  • The compiler reads the abstract class and records every abstract val/var/fun as an unresolved obligation.
  • When a subclass is compiled, the compiler checks that each obligation is satisfied by an override member somewhere in the class (or that the subclass is itself marked abstract, passing the obligation further down).
  • Only once a class in the hierarchy has zero unresolved abstract members can it be instantiated with a constructor call.
  • At runtime, calls to abstract-turned-overridden members are dispatched virtually: even when code holds a reference typed as the abstract base class (like List<Employee> in Example 2), the actual overridden method for the real subclass runs — this is standard polymorphism, and it’s why shapes and staff can be lists of the base type yet still call each item’s own logic.

Common Mistakes

Mistake 1: Trying to instantiate an abstract class directly

abstract class Animal {
    abstract fun sound(): String
}

fun main() {
    val a = Animal()
    println(a.sound())
}

This fails to compile with an error like Cannot create an instance of an abstract class, because Animal has an unresolved sound() with no body to run. Fix it by instantiating a concrete subclass instead:

abstract class Animal {
    abstract fun sound(): String
}

class Dog : Animal() {
    override fun sound(): String = "Woof"
}

fun main() {
    val a: Animal = Dog()
    println(a.sound())
}

Mistake 2: Forgetting to override an abstract member

abstract class Vehicle {
    abstract fun start()
}

class Car : Vehicle() {
    // forgot to override start()
}

This fails with an error similar to Class 'Car' is not abstract and does not implement abstract member 'start'. Either implement the member, or mark Car itself abstract if it genuinely can’t provide one yet:

abstract class Vehicle {
    abstract fun start()
}

class Car : Vehicle() {
    override fun start() {
        println("Engine started")
    }
}

Mistake 3: Reaching for an abstract class when an interface would do

If a hierarchy has no shared constructor state and no shared implemented logic — just a set of method signatures every type must supply — an interface is simpler and also allows a class to adopt several of them at once (Kotlin classes extend only one class, abstract or not, but can implement many interfaces). Reserve abstract classes for the cases that genuinely need shared state, initialization logic, or a template method like Example 3.

Best Practices

  • Use an abstract class when subclasses share constructor state or common implemented logic; use an interface when you’re only defining a contract.
  • Keep the abstract member list small and focused — every abstract member is a mandatory obligation for every future subclass.
  • Mark orchestrating methods (like process() in the template method example) as final (no modifier) so subclasses can’t accidentally break the sequence they encode.
  • Prefer val for abstract properties unless a subclass genuinely needs to reassign the value later.
  • Give abstract methods and properties names that describe the contract (calculateBonus, not doStuff) since callers working through the base type will only see the abstract signature.
  • Use protected for abstract members that are implementation details meant to be called only from within the class hierarchy, not from external code.

Practice Exercises

  • Write an abstract class PaymentMethod with an abstract fun processPayment(amount: Double): String. Create CreditCard and PayPal subclasses with different messages, then call processPayment on a List<PaymentMethod> containing one of each.
  • Write an abstract class Notification(val recipient: String) with an abstract val channel: String and a concrete fun send() that prints "Sending via $channel to $recipient". Create EmailNotification and SmsNotification subclasses.
  • Extend Example 3’s DataProcessor with a second subclass, SquaringProcessor, that squares each number instead of doubling it. Run both processors on the same input and compare the printed results.

Summary

  • An abstract class cannot be instantiated directly and can mix abstract members (no body) with concrete members (with a body).
  • Every concrete subclass must override all inherited abstract members, or itself be declared abstract; the compiler enforces this.
  • Abstract members are implicitly open; use plain open only for members that have a default implementation but may still be overridden.
  • Unlike interfaces, abstract classes can hold constructor parameters, initialized state, and initializer blocks.
  • The template method pattern — a final method that calls several abstract steps — is a common, powerful use of abstract classes.
  • Choose an abstract class over an interface specifically when subclasses need to share state or concrete logic, not just a contract.