A Kotlin Class Is Mostly Its Header


Most of a Kotlin program lives in its classes, and most of a Kotlin class lives in one line: the header. That header doubles as the constructor and declares your properties, which is why a class that would be thirty lines of Java often fits in one. The rest is a handful of conventions around setup, access, and inheritance.

This is part one of two. Here we cover the everyday class — how you declare one, get values into it, run setup logic, control access, and extend it. Part two covers the specialized kinds: data, enum, sealed, object, and friends.

The smallest possible class

This is a complete, valid class:

class Person

No body, no braces, nothing. You create an instance by calling it like a function — note that Kotlin has no new keyword:

val p = Person()

Add a body when you have something to put in it:

class Person {
    fun greet() {
        println("Hello")
    }
}

Properties: the heart of a class

A property is Kotlin’s version of a field plus its getter and setter, declared in one line:

class Person {
    var name: String = "Unknown"
    val species: String = "Homo sapiens"
}

var is read-write, val is read-only. You access both with a dot:

val p = Person()
p.name = "Ada"      // fine — name is a var
println(p.species)  // "Homo sapiens"
// p.species = "..." // compile error — species is a val

Under the hood Kotlin generates a getter (and, for var, a setter). You rarely write them by hand, but they’re there — which matters later when you want to customize them.

The primary constructor

Most classes need values handed in at creation time. The primary constructor goes right in the class header:

class Person(name: String, age: Int)

But there’s a crucial detail. The line above declares constructor parameters, not properties — name and age are not accessible after construction. Add val or var to promote them to properties:

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

Now name and age are real properties:

val p = Person("Ada", 36)
println(p.name)  // "Ada"
p.age = 37       // var, so this works

This one-line “declare and assign properties from the constructor” is something Java only recently approached with records. In Kotlin it’s the default way to write a class.

init blocks: code that runs at construction

The primary constructor can’t contain statements — so where does setup logic go? An init block:

class Person(val name: String) {
    init {
        require(name.isNotBlank()) { "name must not be blank" }
    }
}

init runs when the instance is created. You can have several; they execute top to bottom, interleaved with property initializers:

class Person(val name: String) {
    val initials = name.take(1)   // runs first

    init {
        println("Creating $name")  // runs second
    }
}

A parameter without val/var is still usable here — it just isn’t kept as a property:

class Circle(radius: Double) {
    val area = Math.PI * radius * radius   // radius used, then discarded
}

radius exists only long enough to compute area. After construction, there’s no radius property on the object.

Default and named arguments

Before reaching for more constructors, remember that Kotlin parameters can have defaults:

class Server(
    val host: String = "localhost",
    val port: Int = 8080,
    val useTls: Boolean = false,
)

Combined with named arguments, this replaces the pile of overloaded constructors you’d write in Java:

Server()                                   // all defaults
Server(port = 9090)                        // override just one
Server(host = "example.com", useTls = true)

One declaration, every sensible combination, and the call sites read clearly.

Secondary constructors

Occasionally you need an alternative way to build an object that does more than defaults allow. That’s a secondary constructor, and it must delegate to the primary one with this(...):

class Person(val name: String) {
    var age: Int = 0

    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
}

In practice, default arguments make secondary constructors rare. Reach for them mainly when interoperating with frameworks that expect specific constructor shapes.

Encapsulation: visibility and custom accessors

Properties are public by default, but you can tighten that — including making a property publicly readable but only privately writable:

class BankAccount(initial: Int) {
    var balance: Int = initial
        private set

    fun deposit(amount: Int) {
        balance += amount   // allowed inside the class
    }
}

Outside code can read balance but not assign to it. You can also define a property entirely in terms of a getter — a computed property that holds no backing field:

class BankAccount(var balance: Int) {
    val isOverdrawn: Boolean
        get() = balance < 0
}

isOverdrawn is recomputed every time it’s read. No storage, always consistent with balance.

Inheritance: classes are final by default

Here’s a deliberate difference from Java. In Kotlin, a class cannot be subclassed unless you say so. You opt in with open:

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

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

Three things to notice:

  • The base class is open; without it, Dog : Animal won’t compile.
  • The method you intend to override is also open, and the override is marked with override (not optional — the compiler enforces it).
  • The subclass calls the base constructor right in its header: : Animal(name).

This “final by default” stance encodes a long-standing piece of OO advice — design for inheritance explicitly, or prohibit it — into the language itself.

Abstract classes

When a base class shouldn’t be instantiated on its own and leaves some behavior unspecified, make it abstract:

abstract class Shape {
    abstract fun area(): Double          // no body — subclasses must provide one

    fun describe(): String = "Area is ${area()}"   // concrete, shared
}

class Square(val side: Double) : Shape() {
    override fun area(): Double = side * side
}

Abstract members have no implementation and are implicitly open, so you don’t repeat open. An abstract class can freely mix abstract members with concrete ones — useful when subclasses share most behavior but differ in a few key spots.

Final thoughts

Everything above is the plain class: a header that doubles as a constructor, properties that bundle storage with access, init for setup, and explicit, opt-in inheritance. It’s a small set of rules, but they compose into most of the classes you’ll ever write.

What makes Kotlin’s type system feel rich is the next layer — the specialized class kinds that bake whole behaviors in for you: structural equality with data, fixed sets with enum, closed hierarchies with sealed, singletons with object, and more. That’s part two.