Classes, data classes and extensions

Primary constructors, inheritance and interfaces, then the language features that remove boilerplate: data classes, sealed types and extension functions.

Classes and interfaces

interface Notifier {
    fun send(message: String)
    fun label(): String = this::class.simpleName ?: "notifier"   // default body
}

open class Account(val owner: String, opening: Long = 0) {
    var balance: Long = opening
        private set                                   // readable, not writable outside

    init {
        require(opening >= 0) { "opening must not be negative" }
    }

    open fun deposit(amount: Long) {
        require(amount > 0) { "amount must be positive" }
        balance += amount
    }
}

class Savings(owner: String, opening: Long) : Account(owner, opening) {
    override fun deposit(amount: Long) {
        super.deposit(amount)
    }
}
  • Classes are final by default; mark a class open only if it is designed for inheritance.
  • Properties declared in the primary constructor become fields with implied getters, and var ones also get setters.
  • A custom setter with a narrower visibility (private set) is the idiomatic way to expose read-only state that the class still mutates.

Data classes and sealed types

data class Point(val x: Int, val y: Int)

val p = Point(1, 2)
val q = p.copy(y = 5)              // Point(x=1, y=5)
val (x, y) = p                     // destructuring
println(p)                         // Point(x=1, y=2) - generated toString

sealed interface Shape {
    data class Circle(val radius: Double) : Shape
    data class Rect(val width: Double, val height: Double) : Shape
}

fun area(shape: Shape): Double = when (shape) {
    is Shape.Circle -> Math.PI * shape.radius * shape.radius
    is Shape.Rect -> shape.width * shape.height
}   // no else needed: the compiler proves the branches are exhaustive

A data class must have at least one constructor parameter and generates equals, hashCode, toString, copy and component functions from exactly those parameters. A sealed hierarchy restricts subtypes to the same module, which is what makes exhaustive when possible.

Extension functions and scope functions

// add a function to a type you do not own, without inheritance
fun String.titleCase(): String =
    split(' ').joinToString(" ") { it.lowercase().replaceFirstChar(Char::uppercaseChar) }

fun <T> List<T>.secondOrNull(): T? = if (size > 1) this[1] else null

"hello world".titleCase()          // "Hello World"
listOf(1, 2, 3).secondOrNull()     // 2

// inside the extension, this is the receiver
fun Account.describe(): String = owner + " has " + balance
FunctionReceiver isReturns
letitThe lambda result
runthisThe lambda result
withthisThe lambda result
applythisThe receiver, for configuration
alsoitThe receiver, for side effects
💡
Extensions are resolved statically, so they are not virtual: if a class declares a member function with the same signature, the member always wins and the extension is never called. Use them for convenience, not for polymorphism.

FAQ

Why is my data class equality failing?
Only constructor parameters are used by the generated equals. Anything set in the body is ignored, so two instances can be equal while holding different extra state.
apply or also?
apply when you are configuring the receiver and want to return it, with this in scope. also when you want a side effect such as logging and prefer an explicit it.

Syntax and null safety Coroutines and collections

Last refreshed 2026-09-18.