Kotlin Abstract Classes

Abstract classes provide a base definition that cannot be instantiated directly. They allow you to declare abstract members (properties and methods) without implementation, forcing subclasses to provide concrete behavior.

1. Declaring an Abstract Class

Use the abstract modifier on a class. You cannot create instances of it directly.

abstract class Animal(val name: String) {
  abstract fun speak(): String
}

val a = Animal("Test") // Error: Cannot create an instance of abstract class

2. Abstract Members

Declare abstract functions or properties without a body. Subclasses must override them.

abstract class Shape {
  abstract val area: Double
  abstract fun draw()
}

3. Providing Concrete Members

An abstract class can also include concrete (non-abstract) members with implementation for shared logic.

abstract class Vehicle(val make: String) {
  abstract fun maxSpeed(): Int

  fun info() = "Vehicle make: \$make, max speed: \${maxSpeed()} km/h"
}

4. Subclassing and Overriding

Subclasses must use override to implement abstract members and can also override non-abstract open members.

class Car(make: String, val model: String): Vehicle(make) {
  override fun maxSpeed() = 200
}
val car = Car("Toyota", "Supra")
println(car.info())  // Vehicle make: Toyota, max speed: 200 km/h

5. Constructors in Abstract Classes

Abstract classes can have primary and secondary constructors, which subclasses must delegate to.

abstract class Person(val name: String) {
  abstract val id: String
  constructor(name: String, id: String): this(name) {
    println("Created person \$name with id \$id")
  }
}

class Student(name: String, override val id: String): Person(name, id)

6. Abstract vs Interface

Use abstract classes when you need to share state (properties) or code. Use interfaces for pure behavior contracts, especially when multiple inheritance of behavior is needed.

interface Drivable { fun drive(): String }
abstract class VehicleBase(val make: String): Drivable {
  fun info() = "Make: \$make"
}
// Car inherits shared code and interface behavior
class Car2(make: String): VehicleBase(make) {
  override fun drive() = "Driving \$make"
}

7. Sealed Abstract Classes

You can combine sealed with abstract to restrict subclassing to the same file and force exhaustive handling.

sealed abstract class Result
data class Success(val data: String): Result()
object Failure: Result()

fun handle(r: Result) = when(r) {
  is Success -> "Success: \${r.data}"
  Failure    -> "Failure"
}

8. Visibility and Abstract Members

Abstract members can have visibility modifiers. By default they are public.

abstract class SecretBase {
  protected abstract val secret: String
}

class SecretImpl: SecretBase() {
  override val secret = "TopSecret"
  fun reveal() = secret
}

9. Best Practices

- Use abstract classes when you need shared state or code along with an enforced contract.
- Keep abstract hierarchies shallow to avoid complexity.
- Favor interfaces if only behavior contracts are needed.
- Document abstract members (KDoc) to clarify subclass obligations.
- Provide default implementations for non-abstract methods to reduce duplication.
- Consider composition over inheritance for changeable relationships.
- Leverage sealed abstract classes for closed type hierarchies requiring exhaustive checks.

10. Summary

Abstract classes are powerful for creating base types with a mix of enforced contracts and shared implementation. Use them judiciously to model real-world hierarchies while keeping your design flexible and maintainable.

Previous: Kotlin Inheritance | Next: Kotlin Interface

<
>