A surprising amount of OO modeling in Kotlin happens with constructs that don’t really exist in Java — data class, sealed class, object, value classes. They reshape the kinds of designs that feel natural in the language. This is a working reference for the class-side of Kotlin, from the basics to the bits that change how you model your domain.

Class Declaration

Basic Class

class Greeter {
    fun greet() {
        println("Hello!")
    }
}

// With constructor parameters
class Person(val name: String, var age: Int) {
    fun introduce() {
        println("I'm $name, $age years old")
    }
}

// Empty body can be omitted
class Empty

Visibility and Annotations on Constructor

class Customer public @Inject constructor(name: String) {
    // ...
}

Initializer Blocks

class Customer(name: String) {
    val customerKey: String

    init {
        customerKey = name.uppercase()
        println("Customer initialized: $customerKey")
    }
}

Constructors

Primary Constructor

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

// Private property in constructor
class ParameterizedClass<A>(private val value: A) {
    fun getValue(): A = value
}

Secondary Constructors

class Person(val name: String) {
    var children: MutableList<Person> = mutableListOf()

    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

// Multiple secondary constructors
class MyView : View {
    constructor(ctx: Context) : super(ctx)
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

Private Constructor

class Singleton private constructor() {
    companion object {
        val instance = Singleton()
    }
}

Inheritance

Open Classes

Classes are final by default. Use open to allow inheritance:

open class Animal(val name: String) {
    open fun makeSound() {
        println("Some sound")
    }
}

class Dog(name: String) : Animal(name) {
    override fun makeSound() {
        println("Woof!")
    }
}

Abstract Classes

abstract class Shape(val sides: List<Double>) {
    val perimeter: Double get() = sides.sum()
    abstract fun calculateArea(): Double
}

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

Preventing Further Override

open class Base {
    open fun v() {}
}

class Derived : Base() {
    final override fun v() {}  // Cannot be overridden
}

Properties

Property Override

Properties can be overridden. val can be overridden with var (adding setter):

open class Foo {
    open val x: Int get() = 1
}

class Bar : Foo() {
    override var x: Int = 0  // val -> var is allowed
}

Custom Getters and Setters

class Rectangle(val width: Int, val height: Int) {
    val area: Int
        get() = width * height

    var counter: Int = 0
        set(value) {
            if (value >= 0) field = value
        }

    var setterVisibility: String = "visible"
        private set  // Private setter
}

Late Initialization

For non-null properties initialized after construction:

class MyTest {
    lateinit var subject: TestSubject

    @Before
    fun setup() {
        subject = TestSubject()
    }

    @Test
    fun test() {
        subject.method()  // Direct access, no null check
    }

    fun checkInitialized(): Boolean = ::subject.isInitialized
}

Nested and Inner Classes

Nested Class (Static)

class Outer {
    private val bar: Int = 1

    class Nested {
        fun foo() = 2  // Cannot access bar
    }
}

val demo = Outer.Nested().foo()  // 2

Inner Class

class Outer {
    private val bar: Int = 1

    inner class Inner {
        fun foo() = bar  // Can access outer class members
    }
}

val demo = Outer().Inner().foo()  // 1

Qualified this and super

class A {
    inner class B {
        fun Int.foo() {
            val a = this@A       // A's this
            val b = this@B       // B's this
            val c = this         // Int's this (receiver)
        }
    }
}

class Bar : Foo() {
    inner class Baz {
        fun g() {
            super@Bar.f()  // Calls Foo's f()
        }
    }
}

Interfaces

Basic Interface

interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")  // Default implementation
}

class Button : Clickable {
    override fun click() = println("Button clicked")
}

Interface with Properties

interface Named {
    val name: String
}

class Person(override val name: String) : Named

Multiple Interface Inheritance

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

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

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

Data Classes

Automatically generate equals(), hashCode(), toString(), copy(), and componentN():

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

val john = User("John", 30)
val olderJohn = john.copy(age = 31)

// Destructuring
val (name, age) = john

// In loops
for ((key, value) in map) {
    println("$key -> $value")
}

Returning Multiple Values

data class Result(val value: Int, val status: Status)

fun compute(): Result {
    return Result(42, Status.SUCCESS)
}

val (value, status) = compute()

Sealed Classes

Restricted class hierarchies for exhaustive when:

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
    // No else needed - all cases covered
}

Enum Classes

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

// With properties
enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

// With methods
enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },
    TALKING {
        override fun signal() = WAITING
    };

    abstract fun signal(): ProtocolState
}

// Generic enum access
inline fun <reified T : Enum<T>> printAllValues() {
    print(enumValues<T>().joinToString { it.name })
}

printAllValues<Color>()  // RED, GREEN, BLUE

Object Declarations and Expressions

Object Declaration (Singleton)

object DataManager {
    val data = mutableListOf<String>()

    fun addItem(item: String) {
        data.add(item)
    }
}

// Usage
DataManager.addItem("item")

Companion Objects

Static-like members in classes:

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.create()

// Companion can implement interfaces
interface Factory<T> {
    fun create(): T
}

class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

Object Expressions (Anonymous Objects)

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /* ... */ }
    override fun mouseEntered(e: MouseEvent) { /* ... */ }
})

// Simple object for local use
fun getPoint() = object {
    var x: Int = 0
    var y: Int = 0
}

Visibility Modifiers

Modifier Class Member Top-level
public (default) Visible everywhere Visible everywhere
private Visible in class Visible in file
protected Visible in class and subclasses N/A
internal Visible in same module Visible in same module
open class Base {
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4  // public by default

    protected open fun e() {}
}

Traits Pattern with Extension Functions

interface Logger

fun Logger.log(text: String) {
    Log.d(this.javaClass.simpleName, text)
}

class MyService : Logger {
    fun doSomething() {
        log("Doing something")  // Uses extension
    }
}

Inline Classes (Value Classes)

Wrap a value without runtime overhead:

@JvmInline
value class Password(private val s: String)

fun login(password: Password) { /* ... */ }

// At runtime, Password is just a String
login(Password("secret"))

Best Practices

  1. Prefer composition over inheritance: Use delegation
  2. Make classes final by default: Only open when needed
  3. Use data classes for DTOs: Automatic equals/hashCode
  4. Leverage sealed classes: For restricted hierarchies
  5. Use companion objects wisely: For factory methods and constants
  6. Prefer properties over getters: More Kotlin idiomatic

Wrapping up

Most Kotlin codebases lean heavily on three constructs — data class for values, sealed class for bounded hierarchies, and object for singletons. Get those three to feel natural and the rest of the OO model falls into place quickly.