Polyglot
Languages Kotlin oop
Kotlin § oop

Classes and OOP

Kotlin’s class system admits the conventional OOP machinery: classes with primary and secondary constructors, properties (with custom getters/setters), inheritance (single-class plus multiple-interface), visibility modifiers (public/internal/protected/private), abstract classes, sealed classes (treated separately), open/final for inheritance control, companion objects for type-level members, and interface defaults. The conventional Kotlin discipline: classes are final by default — admit open for inheritable classes; methods are final by default — admit open for overridable. Data classes (treated separately) admit substantial value-type behaviour. The combination — concise primary constructors with property declarations, the final-by-default discipline, the substantial interface support, the companion object for static-like members — is the substance of Kotlin’s class-oriented surface.

Class declarations

The principal form:

class Person(val name: String, var age: Int) {     // primary constructor
    fun greet(): String {
        return "Hello, I am $name"
    }
}

val p = Person("Alice", 30)
println(p.greet())
p.age = 31                                          // mutate

The primary constructor is part of the class header. Parameters with val or var automatically become properties; bare parameters are constructor-only locals.

Properties

class Person {
    val name: String                               // read-only property
    var age: Int = 0                               // mutable property

    constructor(name: String) {
        this.name = name
    }
}

class Counter {
    var count: Int = 0
        private set                                // private setter (custom visibility)

    val isPositive: Boolean                        // computed property
        get() = count > 0
}

val c = Counter()
c.count                                            // OK; public get
// c.count = 1                                     // ERROR: private set

Properties admit custom getters and setters:

class Temperature {
    var celsius: Double = 0.0
        get() = field                              // explicit; default behaviour
        set(value) {
            require(value > -273.15) { "below absolute zero" }
            field = value
        }

    val fahrenheit: Double                         // computed
        get() = celsius * 9 / 5 + 32
}

The field is the backing field — admits the auto-generated storage.

Constructors

Primary constructor

class Person(val name: String, var age: Int)       // header — single-line form

class User(
    val id: Int,
    val name: String,
    val email: String,
    var lastLogin: Instant?
)

For initialisation logic, the init block:

class Person(val name: String) {
    val nameLength: Int

    init {
        require(name.isNotBlank()) { "name must not be blank" }
        nameLength = name.length
    }
}

Secondary constructors

For substantial alternative constructors:

class Rectangle(val width: Int, val height: Int) {
    constructor(side: Int) : this(side, side)      // delegates to primary

    constructor(parent: Rectangle) : this(parent.width, parent.height) {
        // additional logic
    }
}

val r = Rectangle(5, 10)
val sq = Rectangle(5)                              // square via secondary

The secondary constructor must delegate to the primary (directly or via another secondary) using : this(...).

Inheritance

By default, classes are final — admit no inheritance. The open keyword admits inheritance:

open class Animal(val name: String) {
    open fun speak(): String = "..."

    fun rest() = "$name is resting"                // final by default
}

class Dog(name: String, val breed: String) : Animal(name) {
    override fun speak(): String = "Woof!"
                                                   // can't override rest (final)
}

val d = Dog("Rex", "Labrador")
println(d.speak())                                 // "Woof!"
println(d.rest())                                  // "Rex is resting"

The override keyword is required — admits explicit intent.

The open class admits subclassing; open fun admits overriding.

For abstract classes and abstract members:

abstract class Shape {                             // abstract; cannot be instantiated
    abstract fun area(): Double                    // must be overridden
    abstract val name: String                       // abstract property

    fun describe() = "Shape: $name, area: ${area()}"
}

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

Abstract members are implicitly open.

Interfaces

interface Greetable {
    fun greet(): String                            // abstract method
    val name: String                               // abstract property

    fun fullGreeting(): String = "Hello! ${greet()}"  // default implementation
}

class Person(override val name: String) : Greetable {
    override fun greet() = "I am $name"
}

val p = Person("Alice")
p.greet()                                          // "I am Alice"
p.fullGreeting()                                   // "Hello! I am Alice"

Interfaces admit:

  • Abstract methods.
  • Abstract properties (without backing field).
  • Default method implementations.
  • Property getters with logic (no backing field).

A class may implement multiple interfaces:

class Service : Configurable, Logger, ErrorHandler {
    /* ... */
}

For conflict resolution with multiple defaults:

interface A {
    fun foo() = "A"
}

interface B {
    fun foo() = "B"
}

class C : A, B {
    override fun foo() = super<A>.foo() + super<B>.foo()  // qualify
}

println(C().foo())                                 // "AB"

Visibility modifiers

ModifierVisibility
public (default)Visible everywhere
internalVisible within the module
protectedVisible in the class and subclasses
privateVisible in the class only (or file for top-level)
class Account(initialBalance: Double) {
    var balance: Double = initialBalance
        private set                                 // private setter

    private val transactions = mutableListOf<Transaction>()

    internal fun audit(): List<Transaction> = transactions.toList()

    fun deposit(amount: Double) {
        require(amount > 0)
        balance += amount
        transactions.add(Transaction.Deposit(amount))
    }
}

companion object

Inside a class, companion object admits class-level (rather than instance-level) members:

class User(val id: Int, val name: String) {
    companion object {
        const val MAX_NAME_LENGTH = 100

        fun create(name: String): User {
            require(name.length <= MAX_NAME_LENGTH)
            return User(generateId(), name)
        }

        private fun generateId(): Int = /* ... */
    }
}

User.MAX_NAME_LENGTH                               // access via class
User.create("Alice")

The companion object admits the conventional Java-style “static” members.

For named companion objects:

class User {
    companion object Factory {                     // named
        fun create(name: String): User { /* ... */ }
    }
}

User.create("Alice")                               // implicit
User.Factory.create("Alice")                       // explicit

The companion object may implement interfaces:

class User private constructor(val id: Int, val name: String) {
    companion object : Builder<User> {
        override fun build(name: String): User = User(generateId(), name)
    }
}

Methods

class Counter {
    var count: Int = 0
        private set

    fun increment() {
        count++
    }

    fun decrement() {
        count--
    }

    fun reset() {
        count = 0
    }

    override fun toString() = "Counter($count)"
}

Methods are final by defaultoverride requires the parent to mark open (or abstract).

Inner vs nested classes

By default, nested classes do not admit access to outer-instance state:

class Outer {
    private val name = "Outer"

    class Nested {                                 // does NOT have access to name
        fun describe() = "I'm nested"
    }

    inner class Inner {                            // HAS access to name
        fun describe() = "I'm inside $name"
    }
}

Outer.Nested()                                     // construct directly
Outer().Inner()                                    // requires outer instance

The inner keyword admits access to outer state — at the cost of holding a reference.

this

The this refers to the enclosing receiver:

class Person(val name: String) {
    fun greet() {
        println("Hello, ${this.name}")             // explicit this
        println("Hello, $name")                     // implicit this
    }
}

class Outer {
    val name = "Outer"

    inner class Inner {
        val name = "Inner"

        fun describe() {
            println(this.name)                     // "Inner"
            println(this@Outer.name)               // "Outer"
        }
    }
}

The qualified this@ClassName admits accessing outer scopes from inner classes.

object declarations

Singletons:

object Logger {
    private var enabled = false

    fun setEnabled(value: Boolean) {
        enabled = value
    }

    fun log(message: String) {
        if (enabled) println(message)
    }
}

Logger.log("hello")

Treated in Data classes and objects.

data class

For value-typed records:

data class Person(val name: String, val age: Int)

Treated in Data classes and objects.

sealed types

For closed hierarchies:

sealed class Result<out T>
class Success<T>(val value: T) : Result<T>()
class Failure(val error: String) : Result<Nothing>()

Treated in Sealed types.

Common patterns

Primary constructor with init

class Email(val address: String) {
    init {
        require(address.contains("@")) { "Invalid email: $address" }
    }
}

Property with custom getter

class Circle(val radius: Double) {
    val area: Double
        get() = Math.PI * radius * radius

    val diameter: Double
        get() = radius * 2
}

Property with backing field

class TextField {
    var text: String = ""
        set(value) {
            field = value.trim()                   // mutate via field
            notifyListeners(field)
        }
}

Builder with secondary constructor

class HttpRequest(
    val url: String,
    val method: String = "GET",
    val headers: Map<String, String> = emptyMap(),
    val body: ByteArray? = null
) {
    constructor(url: String, method: String, headersList: List<Pair<String, String>>) :
            this(url, method, headersList.toMap())
}

val req = HttpRequest(
    url = "https://example.com",
    method = "POST",
    headers = mapOf("Content-Type" to "application/json"),
    body = jsonBytes
)

Singleton via object

object DatabaseConnection {
    private val connectionPool = ConnectionPool()

    fun connect(): Connection = connectionPool.acquire()
    fun release(connection: Connection) { connectionPool.release(connection) }
}

DatabaseConnection.connect()

Companion factory

class Token private constructor(val value: String, val expiresAt: Instant) {
    companion object {
        fun create(): Token {
            return Token(generateRandom(), Instant.now() + 1.hours)
        }

        fun parse(s: String): Token? {
            // ...
        }
    }
}

The private constructor admits factory-only construction.

Abstract class with template method

abstract class Report {
    fun generate(): String = buildString {
        append("==== ${title()} ====\n")
        append(body())
        append("\n==== End ====")
    }

    abstract fun title(): String
    abstract fun body(): String
}

class SalesReport : Report() {
    override fun title() = "Sales"
    override fun body() = "Sales data: ..."
}

Interface with default

interface Logger {
    val tag: String

    fun debug(message: String) = println("[$tag] DEBUG: $message")
    fun info(message: String) = println("[$tag] INFO: $message")
    fun warn(message: String) = println("[$tag] WARN: $message")
    fun error(message: String) = println("[$tag] ERROR: $message")
}

class MyService : Logger {
    override val tag = "MyService"
}

MyService().info("started")                        // [MyService] INFO: started

Visibility-controlled API

class API {
    val publicEndpoint = "/v1/public"
    internal val internalEndpoint = "/v1/internal"
    private val secret = "..."

    fun publicMethod() { /* ... */ }
    private fun privateMethod() { /* ... */ }
}

Inheritance with constructor delegation

open class Animal(val name: String) {
    open fun speak() = "..."
}

class Dog(name: String, val breed: String) : Animal(name) {
    override fun speak() = "Woof!"
}

class Puppy(name: String, breed: String) : Dog(name, breed) {
    override fun speak() = "Yip!"
}

Multiple interface implementation

interface Drawable {
    fun draw()
}

interface Resizable {
    fun resize(width: Int, height: Int)
}

class Widget : Drawable, Resizable {
    override fun draw() { /* ... */ }
    override fun resize(width: Int, height: Int) { /* ... */ }
}

Diamond inheritance with super<>

interface A { fun foo() = "A" }
interface B { fun foo() = "B" }

class C : A, B {
    override fun foo() = "${super<A>.foo()} + ${super<B>.foo()}"
}

Private constructor for factory

class UserId private constructor(val value: Int) {
    companion object {
        fun fromInt(n: Int): UserId? {
            if (n < 0) return null
            return UserId(n)
        }
    }
}

Inner class for tight coupling

class Container<T> {
    private val items = mutableListOf<T>()

    inner class Iterator {                         // tightly coupled to Container
        private var index = 0

        fun hasNext() = index < items.size
        fun next() = items[index++]
    }

    fun iterator() = Iterator()
}

Property delegation (preview)

class Service {
    val config: Config by lazy {
        loadConfig()                                // computed on first access
    }
}

Treated in Property delegation.

A note on the conventional discipline

The contemporary Kotlin OOP advice:

  • Use data class for value-typed records.
  • Use class for substantial reference-typed entities.
  • Use object for singletons.
  • Use sealed class / sealed interface for closed hierarchies.
  • Default to final — admit open only when inheritance is intended.
  • Use private constructors with companion object for factory patterns.
  • Use primary constructors for substantial conciseness.
  • Use init blocks for validation.
  • Use computed properties for derived values.
  • Use property setters with backing field for validation.
  • Prefer interfaces over abstract classes — admit substantial multiple inheritance.
  • Use internal for module-only APIs.
  • Use inner sparingly — pay the outer-reference cost only when needed.

The combination — primary-constructor classes with property declarations, the final-by-default inheritance discipline, the substantial interface mechanism with default methods, the companion object for type-level members, the object for singletons, the substantial visibility modifiers — is the substance of Kotlin’s class-oriented surface. The discipline produces concise, well-encapsulated code with substantial flexibility for both classical OOP and the conventional Kotlin combination of OOP and functional patterns.