Kotlin Classes and Objects

Model real things with classes: primary constructors, properties with val and var, methods, init blocks, custom getters and setters, and private members.

Lesson 13 of 43Intermediate19 min read

So far you've stored data in separate variables and lists. Real programs are about things: a bank account with a balance, a player with a name and a score, a product with a price and stock. A class describes one kind of thing, bundling its data (properties) with the actions it can perform (methods). An object is one particular thing made from that class: Sam's account, or the backpack product.

Kotlin's classes are remarkably concise. Properties can be declared right in the constructor, so a class that would take twenty lines in some languages fits on one. Objects are created without a new keyword, and properties replace the getter and setter methods you may know from Java.

In this lesson you'll write classes with constructors, properties and methods, validate data in init blocks, control access with private, and add custom getters and setters. Classes are the foundation for the rest of the intermediate stage.

A first class

Kotlin
class Player(val name: String, var score: Int = 0) {
    fun addPoints(points: Int) {
        score += points
    }
}

val p = Player("Ana")
p.addPoints(10)
println("${p.name}: ${p.score}")    // Ana: 10
  • The brackets after the class name are the primary constructor.
  • val and var in the constructor create properties: name can't change, score can.
  • Parameters can have default values, just like functions.
  • Create an object by calling the class like a function. There's no new.
  • Methods are functions inside the class and can use the properties directly.

init blocks and validation

Code in an init block runs when an object is created, which makes it the place to check that the data is valid:

Kotlin
class Temperature(val celsius: Double) {
    init {
        require(celsius >= -273.15) { "Below absolute zero: $celsius" }
    }
}

require(condition) { message } throws an IllegalArgumentException if the condition is false. Checking data at creation means every object of the class is guaranteed to be valid, so the rest of your code doesn't have to keep checking.

Properties: getters, setters and visibility

A property can calculate its value every time it's read with a custom getter:

Kotlin
class Rectangle(val width: Double, val height: Double) {
    val area: Double get() = width * height
}

A custom setter runs when a var is assigned; field is the stored value:

Kotlin
var name: String = ""
    set(value) { field = value.trim() }

Control who can see what with visibility modifiers: public (the default, visible everywhere), private (only inside the class), protected (the class and its subclasses) and internal (the same module). var balance = 0.0 private set lets anyone read the balance but only the class change it.

Secondary constructors and lateinit

Most classes need only the primary constructor, often with default values. If you need another way to create objects, add a secondary constructor with constructor(...), which must call the primary one with this(...).

lateinit var declares a non-null property that will be set later, for example after a screen is created in Android. Reading it before it's set throws an exception, so use it sparingly; a nullable type or a default value is usually safer.

Variables hold references to objects

When you assign an object to another variable, Kotlin doesn't copy it. Both names point at the same object, so a change through one is visible through the other:

Kotlin
val a = Player("Ana")
val b = a
b.score = 20
println(a.score)        // 20

fun bonus(p: Player) { p.score += 5 }
bonus(a)
println(a.score)        // 25

The same happens when you pass an object to a function: bonus receives a reference to Ana's player, not a copy, so its change sticks. The val in val a only stops a from being pointed at a different player; it doesn't freeze the player's own var properties.

This also explains how ordinary classes compare. === asks "are these the very same object?", and for a plain class, == asks the same question unless you override equals. Two separate players with identical names and scores are not equal:

Kotlin
println(Player("Ana", 25) == Player("Ana", 25))   // false
println(a === b)                                  // true

Printing a plain object shows its class name and a code such as Point@1b6d3586, which isn't much help. Override toString to choose what's printed:

Kotlin
class Player(val name: String, var score: Int = 0) {
    override fun toString() = "Player($name, $score)"
}

If you find yourself writing equals and toString by hand for a class that mainly holds data, a data class generates both for you.

Kotlin Classes Syntax

Class syntax

class Name(val a: Type, var b: Type = default) {
    init { require(...) { "message" } }
    val derived: Type get() = ...
    var x: Int = 0
        private set
    fun method() { }
    private fun helper() { }
}
val obj = Name(a, b)
obj.method()

Kotlin Classes Summary

  • A class bundles properties and methods; an object is one instance, created without new.
  • val and var in the primary constructor declare properties directly.
  • init blocks run at creation and are the place for require checks.
  • Custom getters calculate values; custom setters use field; private set restricts writing.
  • Visibility: public by default, private, protected and internal.

Kotlin Classes Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A bank account

Kotlin
class Account(val owner: String, initial: Double = 0.0) {
    var balance = initial
        private set

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

    fun withdraw(amount: Double): Boolean {
        if (amount > balance) return false
        balance -= amount
        return true
    }
}

fun main() {
    val acc = Account("Sam", 100.0)
    acc.deposit(50.0)
    println(acc.withdraw(30.0))
    println(acc.withdraw(500.0))
    println("${acc.owner} has £${acc.balance}")
}
Output
true
false
Sam has £120.0
Line-by-line explanation
  1. Line 1initial isn't a property, just a constructor parameter used to set balance.
  2. Lines 2 to 3Anyone can read balance, but only the class can change it.
  3. Line 6require rejects negative deposits.
  4. Line 21The withdrawal fails because the balance is too low.
Example 2

Validation and computed properties

Kotlin
class Rectangle(val width: Double, val height: Double) {
    init {
        require(width > 0 && height > 0) { "Sides must be positive" }
    }

    val area: Double get() = width * height
    val isSquare: Boolean get() = width == height

    fun describe() = "${width}x$height, area $area"
}

fun main() {
    val r = Rectangle(4.0, 2.5)
    println(r.describe())
    println(r.isSquare)

    try {
        Rectangle(-1.0, 3.0)
    } catch (e: IllegalArgumentException) {
        println("Error: ${e.message}")
    }
}
Output
4.0x2.5, area 10.0
false
Error: Sides must be positive
Line-by-line explanation
  1. Lines 2 to 4init runs for every new Rectangle and rejects invalid sizes.
  2. Lines 6 to 7Computed properties have no stored value; they're worked out when read.
  3. Line 9${width} needs braces because an x follows straight after the name.
  4. Lines 17 to 21Creating an invalid object throws, which try and catch handle; the exceptions lesson covers this fully.
Example 3

A custom setter

Kotlin
class Profile(name: String) {
    var name: String = name.trim()
        set(value) {
            field = value.trim().replaceFirstChar { it.uppercase() }
        }

    var visits = 0
        private set

    fun visit() { visits++ }
}

fun main() {
    val p = Profile("  ada ")
    println("[${p.name}]")
    p.name = "  grace hopper"
    println("[${p.name}]")
    p.visit(); p.visit()
    println(p.visits)
}
Output
[ada]
[Grace hopper]
2
Line-by-line explanation
  1. Line 2The initial value is trimmed, but the setter doesn't run for the initial value, so it isn't capitalised.
  2. Lines 3 to 5Every later assignment runs the setter; field is the stored value.
  3. Line 18Two statements on one line need a semicolon between them.

Common Kotlin Classes Mistakes

Using new

Don't
val p = new Player("Ana")      // error: new isn't a Kotlin keyword
Do
val p = Player("Ana")

Forgetting val or var in the constructor

Don't
class User(name: String)
println(User("Ana").name)       // error: Unresolved reference: name
Do
class User(val name: String)

Without val or var, a constructor parameter isn't a property.

A setter that calls itself

Don't
var age: Int = 0
    set(value) { age = maxOf(0, value) }    // infinite recursion
Do
var age: Int = 0
    set(value) { field = maxOf(0, value) }

Kotlin Classes in Real Projects

Classes in Kotlin apps

Android apps model their data with classes such as User, Message and Order, and their behaviour with classes like ViewModel and Repository. Server frameworks like Spring Boot and Ktor organise endpoints and services as classes too. Kotlin's compact constructors are a big part of why Kotlin files are typically much shorter than the equivalent Java.

Kotlin Classes FAQ

What is the difference between a class and an object in Kotlin?

A class is the blueprint: it describes which properties and functions something has. An object, or instance, is one concrete thing built from that blueprint, such as Player("Ana"). You can create as many objects from one class as you like, each with its own property values.

What is the difference between a property and a constructor parameter?

A constructor parameter written with val or var becomes a property that lives as long as the object. Without val or var, it's a plain parameter, available only in init blocks and property initialisers. Use plain parameters for values you only need while setting the object up.

When should I use lateinit instead of a nullable property?

Use lateinit var for a non-null value that's guaranteed to be set before it's read but can't be set in the constructor, such as something a framework injects. If the value can genuinely be missing, use a nullable type. You can check ::name.isInitialized inside the class, but needing that check often means a nullable type would be clearer.

Are Kotlin classes public by default?

Yes. Classes, functions and properties are public unless you say otherwise. Mark details as private so outside code can't depend on them, and use internal for things that should be visible across your module but not to code that uses it as a library.

How do I make a read-only property that the class can still change?

Declare it as a var with a private setter: var balance = 0.0 followed by private set on the next line. Outside code can read account.balance but can't assign to it, while functions inside the class, such as deposit, can update it.

Kotlin Classes Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the constructor so name becomes a read-only property.

class City( name: String)
Predict the outputeasy

What does this print?

class Counter(var n: Int = 0) {
    fun up() { n++ }
}
val c = Counter(5)
c.up(); c.up()
println(c.n)
Multiple choicemedium

What does private set on a property do?

Multiple choicemedium

Where should you check that constructor values are valid?

Predict the outputhard

What does this print?

class Box(val w: Int, val h: Int) {
    val area get() = w * h
}
val b = Box(3, 4)
println(b.area + Box(2, 2).area)

Kotlin Classes Quiz

5 questions to check what you learned.

  1. How do you create an object of class Player?

    • Player()
    • new Player()
    • Player.create()
    • create Player()
  2. What is field inside a custom setter?

    • The class name
    • A new variable
    • The property's stored value
    • The parameter
  3. What is the default visibility of a Kotlin class member?

    • private
    • public
    • internal
    • protected
  4. What does require(x > 0) { "msg" } do when x is -1?

    • Returns false
    • Prints the message
    • Sets x to 0
    • Throws an IllegalArgumentException with the message
  5. What's the risk of lateinit var?

    • It makes the class slower
    • Reading it before it's assigned throws an exception
    • It can only hold Int values
    • It's always null