Free Handbook · Every example compiled & verified

Classes & Objects

Kotlin classes from the ground up: constructors, init blocks, properties with custom getters and setters, visibility, companions and inner classes.

0 / 136 lessons🔥 0 day streak
ShareXLinkedIn

Module 06 · what you'll be able to do

  • Declare a class with a primary constructor, default values, a secondary constructor and init blocks, and predict the order they run in
  • Write properties with custom getters and validating setters using the field backing field
  • Hide state with private, protected and internal, and expose it safely with private set
  • Use a companion object for factory functions and an object expression for a one-off implementation
  • Explain the difference between a nested and an inner class, and write correct equals, hashCode and toString by hand
01

Classes and primary constructors

A class is a blueprint; an object is one thing built from it. In Kotlin the most common constructor is written in the class header itself — the primary constructor. Putting val or var in front of a parameter turns it into a property in one step, so a class that would be twenty lines of Java is one line here. There is no new keyword: you call the class like a function.

kotlinMain.kt
class User(val name: String, var age: Int, val country: String = "IN")

fun main() {
    val ada = User("Ada", 36)
    val linus = User(name = "Linus", age = 28, country = "FI")
    ada.age += 1
    println("${ada.name} ${ada.age} ${ada.country}")
    println("${linus.name} ${linus.age} ${linus.country}")
}
Outputcompiled & run with real Kotlin
Ada 37 IN
Linus 28 FI
Your turn

Remove val from name and read the error you get on ada.name. A parameter without val/var is only visible during construction.

Code that must run when an object is created goes in an init block. A class can have several; they run top to bottom, interleaved with property initialisers in the order they appear in the source. A secondary constructor (constructor(...) in the body) is an alternative way in, and it must delegate to the primary constructor with : this(...) — so the primary constructor and every init block always run first.

kotlinMain.kt
class Rect(val width: Int, val height: Int) {
    val area = width * height

    init {
        require(width > 0 && height > 0) { "sides must be positive" }
        println("init: ${width}x$height, area $area")
    }

    constructor(side: Int) : this(side, side) {
        println("secondary: a square")
    }
}

fun main() {
    Rect(3, 4)
    Rect(5)
    val bad = runCatching { Rect(0, 2) }
    println(bad.exceptionOrNull()?.message)
}
Outputcompiled & run with real Kotlin
init: 3x4, area 12
init: 5x5, area 25
secondary: a square
sides must be positive

In Kotlin, default arguments usually replace secondary constructors; keep secondary constructors for Java interop or genuinely different inputs.

VisualizeConstruction order for Rect(5)Step 1 / 7
class Rect(val width: Int, val height: Int) {
val area = width * height
init { println("init $area") }
constructor(side: Int) : this(side, side) {
println("secondary")
}
}
val r = Rect(5)
Line 8

Rect(5) matches the secondary constructor (one Int).

Variables now
side5
All 7 steps as a table
StepLineWhat happenedVariables now
18Rect(5) matches the secondary constructor (one Int).side = 5
24Before its own body runs, the secondary constructor delegates with this(side, side) to the primary constructor.
31The primary constructor stores the two properties.width = 5 height = 5
42Property initialisers and init blocks run in source order. First area.area = 25
53Then the init block.
65Only now does the secondary constructor's own body run.
78The finished object is assigned.r = Rect(5x5)
Error you will hit

Missing a constructor argument

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

fun main() {
    val u = User("Ada")
    println(u.name)
}
Main.kt:4:13: error: no value passed for parameter 'age'.
    val u = User("Ada")
            ^^^^
Why the compiler said that

Every primary-constructor parameter without a default value is required. Kotlin never fills in 0 or null for you.

The fix

Pass the argument, or give the parameter a default in the class header if a sensible one exists.

kotlin
class User(val name: String, val age: Int = 0)

fun main() {
    val u = User("Ada")
    println(u.name)
}
02

Properties: custom getters, setters and backing fields

A Kotlin property is not a bare field: it is a field plus a getter (and a setter for var). You normally never see them, but you can write your own. A custom getter computes the value on every read and needs no storage. A custom setter can validate or normalise input; inside it, the keyword field refers to the real stored value, called the backing field.

kotlinMain.kt
class Temperature(celsius: Double) {
    var celsius: Double = celsius
        set(value) {
            require(value >= -273.15) { "below absolute zero: $value" }
            field = value
        }

    val fahrenheit: Double
        get() = celsius * 9 / 5 + 32      // computed, no backing field

    var label: String = ""
        set(value) { field = value.trim().lowercase() }
}

fun main() {
    val t = Temperature(20.0)
    println(t.fahrenheit)
    t.celsius = 100.0
    println(t.fahrenheit)
    t.label = "  Boiling "
    println("[${t.label}]")
    println(runCatching { t.celsius = -300.0 }.exceptionOrNull()?.message)
}
Outputcompiled & run with real Kotlin
68.0
212.0
[boiling]
below absolute zero: -300.0
Use field, not the property name, inside a setter
Writing celsius = value inside the setter of celsius calls the setter again, which calls it again, until the program dies with StackOverflowError. Inside an accessor, field is the only way to touch the stored value directly.

The most common real-world pattern is a property the whole world can read but only the class can change: var count = 0; private set. Another is a property that cannot be set in the constructor but will be set before use — lateinit var — common in frameworks that inject values after construction. Reading a lateinit property before it is assigned throws UninitializedPropertyAccessException.

kotlinMain.kt
class Counter {
    var count = 0
        private set

    fun increment() { count++ }
}

class Screen {
    lateinit var title: String
    fun isReady() = this::title.isInitialized
}

fun main() {
    val c = Counter()
    repeat(3) { c.increment() }
    println(c.count)
    // c.count = 10   // error: the setter is private

    val s = Screen()
    println(s.isReady())
    s.title = "Home"
    println("${s.isReady()} ${s.title}")
}
Outputcompiled & run with real Kotlin
3
false
true Home
Error you will hit

A val cannot have a setter

kotlin
class Circle(r: Double) {
    val radius = r
        set(value) { field = value }
}

fun main() = println(Circle(1.0).radius)
Main.kt:3:9: error: a 'val' property cannot have a setter.
        set(value) { field = value }
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Why the compiler said that

val means "read-only from the outside and after construction", so there is nothing for a setter to do. The compiler rejects the contradiction.

The fix

If the value must change, make it a var. If only the class should change it, use var with private set.

kotlin
class Circle(r: Double) {
    var radius = r
        private set
    fun grow() { radius *= 2 }
}

fun main() = println(Circle(1.0).apply { grow() }.radius)
03

Visibility modifiers: public, private, protected, internal

Everything in Kotlin is public unless you say otherwise. Hiding the parts callers should not touch is what keeps a class correct: if balance can only change through deposit and withdraw, those two functions are the only places a bug can hide.

A module is one compilation unit: a Gradle or Maven module, an IntelliJ module, or one kotlinc run.
ModifierClass member is visible toTop-level declaration is visible to
public (default)everyoneeveryone
internalanyone in the same moduleanyone in the same module
protectedthe class and its subclassesnot allowed
privateonly inside the classonly inside the same file
kotlinMain.kt
class BankAccount(val owner: String) {
    var balance = 0
        private set
    private val history = mutableListOf<String>()

    fun deposit(amount: Int) {
        require(amount > 0) { "amount must be positive" }
        balance += amount
        log("+$amount")
    }

    fun withdraw(amount: Int): Boolean {
        if (amount > balance) return false
        balance -= amount
        log("-$amount")
        return true
    }

    fun statement(): List<String> = history.toList()
    private fun log(entry: String) { history += entry }
}

fun main() {
    val acc = BankAccount("Ada")
    acc.deposit(100)
    println(acc.withdraw(250))
    println(acc.withdraw(30))
    println("${acc.owner}: ${acc.balance} ${acc.statement()}")
}
Outputcompiled & run with real Kotlin
false
true
Ada: 70 [+100, -30]
Your turn

Make statement() return history directly and think about what a caller could now do with a cast.

Error you will hit

Touching a private member from outside

kotlin
class Account {
    private var balance = 0
    fun deposit(amount: Int) { balance += amount }
}

fun main() {
    val acc = Account()
    acc.deposit(50)
    acc.balance = 1_000_000
}
Main.kt:9:9: error: cannot access 'var balance: Int': it is private in 'Account'.
    acc.balance = 1_000_000
        ^^^^^^^
Why the compiler said that

The compiler enforces visibility at every call site. private members exist only inside the class body.

The fix

Go through the class's public API. If callers genuinely need to read the value, expose it with a public getter and a private setter instead of making the whole property public.

kotlin
class Account {
    var balance = 0
        private set
    fun deposit(amount: Int) { balance += amount }
}

fun main() {
    val acc = Account()
    acc.deposit(50)
    println(acc.balance)
}
04

object declarations, companion objects and object expressions

Kotlin has no static keyword. Instead, object declares a class with exactly one instance (a singleton), and a companion object inside a class holds members you call on the class name itself — factory functions, constants, parsing helpers. Module 08 (Data, Sealed & Enum Classes) shows data object for sealed hierarchies; here is the everyday class-level use.

kotlinMain.kt
class Money private constructor(val paise: Long) {
    companion object {
        const val PAISE_PER_RUPEE = 100
        val ZERO = Money(0)
        fun rupees(r: Long) = Money(r * PAISE_PER_RUPEE)
        fun parse(text: String): Money? =
            text.removePrefix("Rs ").toLongOrNull()?.let { rupees(it) }
    }
    operator fun plus(other: Money) = Money(paise + other.paise)
    override fun toString() = "Rs ${paise / PAISE_PER_RUPEE}.${(paise % PAISE_PER_RUPEE).toString().padStart(2, '0')}"
}

object IdGenerator {
    private var last = 0
    fun next() = "ID-${++last}"
}

fun main() {
    println(Money.rupees(5) + Money.parse("Rs 20")!!)
    println(Money.ZERO)
    println(Money.parse("twenty"))
    println(IdGenerator.next() + " " + IdGenerator.next())
}
Outputcompiled & run with real Kotlin
Rs 25.00
Rs 0.00
null
ID-1 ID-2

The private constructor forces every caller through the named factories, which is where validation lives.

An object expression — object : SomeInterface { ... } — creates a one-off anonymous object on the spot, the Kotlin version of a Java anonymous class. It is handy for a small listener or comparator that is used in exactly one place.

kotlinMain.kt
fun main() {
    val byLength = object : Comparator<String> {
        override fun compare(a: String, b: String) = a.length - b.length
    }
    println(listOf("kiwi", "fig", "banana").sortedWith(byLength))

    val point = object {
        val x = 3
        val y = 4
    }
    println(point.x * point.x + point.y * point.y)
}
Outputcompiled & run with real Kotlin
[fig, kiwi, banana]
25
05

Nested vs inner classes

A class declared inside another class is nested by default: it is just a class that lives in the outer one's namespace, with no link to any outer instance (like a Java static nested class). Mark it inner and every instance carries a reference to the outer object that created it, so it can read the outer's properties. Inside an inner class, this@Outer names that outer object.

kotlinMain.kt
class Playlist(val name: String) {
    private val songs = mutableListOf<String>()

    class Builder {                     // nested: no outer instance
        private val items = mutableListOf<String>()
        fun add(s: String) = apply { items += s }
        fun build(name: String) = Playlist(name).also { it.songs += items }
    }

    inner class Cursor {                // inner: bound to one Playlist
        private var i = 0
        fun next(): String? = songs.getOrNull(i++)?.let { "$it (from ${[email protected]})" }
    }
}

fun main() {
    val p = Playlist.Builder().add("Intro").add("Outro").build("Focus")
    val cursor = p.Cursor()             // needs an outer instance
    println(cursor.next())
    println(cursor.next())
    println(cursor.next())
}
Outputcompiled & run with real Kotlin
Intro (from Focus)
Outro (from Focus)
null
Error you will hit

A nested class cannot see the outer object

kotlin
class Car(val brand: String) {
    class Engine {
        fun describe() = "Engine of $brand"
    }
}

fun main() = println(Car.Engine().describe())
Main.kt:3:38: error: outer class 'class Car : Any' of non-inner class cannot be used as receiver.
        fun describe() = "Engine of $brand"
                                     ^^^^^
Why the compiler said that

Engine is nested, so an Engine is not attached to any particular Car. There is no brand for it to read.

The fix

Mark it inner and create it from a car (car.Engine()), or keep it nested and pass the data it needs through its constructor. Prefer the second when you can: inner classes keep the outer object alive as long as they live, a classic source of memory leaks on Android.

kotlin
class Car(val brand: String) {
    inner class Engine {
        fun describe() = "Engine of $brand"
    }
}

fun main() = println(Car("Volvo").Engine().describe())
06

equals, hashCode and toString in plain classes

Every class inherits three functions from Any. By default equals (which == calls) compares identity — are these the same object? — hashCode is based on identity too, and toString prints the class name and a hash. === always compares identity and cannot be overridden. A data class (Module 08) generates value-based versions for you; for a plain class, you write them yourself.

kotlinMain.kt
class Isbn(val code: String) {
    override fun equals(other: Any?): Boolean =
        other is Isbn && other.code == code

    override fun hashCode(): Int = code.hashCode()

    override fun toString() = "Isbn($code)"
}

class Tag(val name: String)       // no overrides

fun main() {
    val a = Isbn("978-0")
    val b = Isbn("978-0")
    println(a == b)
    println(a === b)
    println(setOf(a, b).size)
    println(a)

    println(Tag("x") == Tag("x"))
    println(setOf(Tag("x"), Tag("x")).size)
}
Outputcompiled & run with real Kotlin
true
false
1
Isbn(978-0)
false
2
Your turn

Delete the hashCode override and run it: a == b is still true, but the set now (almost always) holds two elements.

Override equals and hashCode together
Hash-based collections (HashSet, HashMap, setOf, mapOf) look up the hashCode bucket first and only then call equals. If two equal objects have different hash codes, the set never compares them and stores both. The rule: equal objects must have equal hash codes. And base both on values that do not change while the object sits in a set.
Value classes, briefly
When a class exists only to give one value a meaningful type — a UserId wrapping a Long — mark it @JvmInline value class. You get a distinct type at compile time and value-based equality, and the JVM usually stores just the raw value. Module 08 (Data, Sealed & Enum Classes) covers them in full.
primary constructor
The constructor in the class header; val/var parameters become properties.
secondary constructor
An extra constructor(...) in the body that must delegate to the primary one with this(...).
init block
Code that runs during construction, in source order with property initialisers.
backing field
The stored value behind a property, reachable as field inside its getter or setter.
private set
Makes a property readable everywhere but writable only inside the class.
lateinit
A non-null var assigned after construction; reading it early throws.
internal
Visible anywhere in the same module, hidden from other modules.
companion object
An object inside a class whose members are called on the class name; Kotlin's replacement for static.
object expression
An anonymous one-off object, often implementing an interface: object : Comparator<T> { ... }.
inner class
A nested class that holds a reference to an instance of its outer class.
structural vs referential equality
== calls equals (value); === checks whether two references are the same object.
Quick check

A class has a primary constructor, then val a = ..., then init { }, then a secondary constructor with a body. When you call the secondary constructor, which runs last?

Quick check

You override equals in a plain class but not hashCode. What happens when you put two equal instances into setOf(a, b)?

Frequently asked questions

What is the difference between a primary and a secondary constructor in Kotlin?
The primary constructor is part of the class header and can declare properties directly with val or var. A secondary constructor is declared in the class body with the constructor keyword and must call the primary constructor. Default arguments remove the need for most secondary constructors.
Does Kotlin have static methods?
Not as a keyword. Use top-level functions for general helpers, and a companion object for functions and constants that belong to a class, such as factory methods. Add @JvmStatic if Java callers need a real static method.
What is the field keyword in Kotlin?
field refers to a property's backing field — the actual stored value — and can only be used inside that property's getter or setter. Using the property name there instead would call the accessor recursively.

Finish the Kotlin handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.