Classes & Constructors

Kotlin · Lesson 14 of 29 7 min read Updated

So far your data has lived in loose variables and collections. Real programs deal with things: bank accounts, students, products, train bookings. Each thing has data (an account has an owner and a balance) and behaviour (you can deposit and withdraw). A class describes one kind of thing, and an object (or instance) is one actual thing made from that class.

Kotlin classes are much shorter than classes in Java because the constructor and the properties can be declared in a single line. In this lesson you will write classes, add behaviour and validation, control who can change what, and learn the newer explicit backing fields from Kotlin 2.4.

A class in one line

The simplest class has a name and a primary constructor: the parameter list right after the class name. Writing val or var in front of a parameter turns it into a property that every object stores.

Kotlin
class Student(val name: String, val rollNo: Int, var marks: Int)

fun main() {
    val priya = Student("Priya", 12, 78)
    println("${priya.name} (roll ${priya.rollNo}) scored ${priya.marks}")

    priya.marks = 84
    println("Updated marks: ${priya.marks}")
    // priya.name = "Isha"   // does not compile: name is a val
}
Output
Priya (roll 12) scored 78
Updated marks: 84

You create an object by calling the class like a function: Student("Priya", 12, 78). There is no new keyword in Kotlin. Each object has its own copy of every property, and you reach them with a dot. A val property is read-only after creation; a var property can be changed.

Class names use PascalCase: each word starts with a capital letter, as in BankAccount.

Methods, init blocks and private setters

A class body in braces can hold more properties, member functions (often called methods) and init blocks. An init block runs when an object is created, which makes it the right place for validation.

Kotlin
class BankAccount(val owner: String, initialBalance: Int = 0) {
    var balance = initialBalance
        private set

    init {
        require(initialBalance >= 0) { "Opening balance cannot be negative" }
    }

    val isPremium: Boolean
        get() = balance >= 100_000

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

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

    override fun toString() = "$owner: Rs $balance"
}

fun main() {
    val account = BankAccount("Rohan", 5_000)
    account.deposit(2_500)
    println(account.withdraw(10_000))
    println(account.withdraw(1_500))
    println(account)
    println(account.isPremium)
    // account.balance = 1_000_000   // does not compile: the setter is private
}
Output
false
true
Rohan: Rs 6000
false

There is a lot in this class, so take it piece by piece:

  • initialBalance has no val, so it is just a constructor parameter, used to set up balance, and not stored separately.
  • private set means anyone can read balance, but only code inside the class can change it. Money moves only through deposit and withdraw, which check the rules. This idea of protecting data is called encapsulation.
  • require(condition) { message } throws an IllegalArgumentException with your message if the condition is false, so an account with a negative balance can never exist.
  • isPremium has a custom getter: it has no stored value and is calculated every time you read it.
  • toString decides what println(account) shows. Without it, you would see the class name and a code such as BankAccount@1b6d3586. The override keyword is explained in the next lesson.

Property initialisers and init blocks run from top to bottom, in the order they appear in the class body.

Custom setters and field

A property can also have a custom setter that runs whenever someone assigns a value. Inside it, the word field refers to the stored value (the backing field).

Kotlin
class AirConditioner {
    var temperature = 24
        set(value) {
            field = value.coerceIn(16, 30)
        }
}

fun main() {
    val ac = AirConditioner()
    ac.temperature = 18
    println(ac.temperature)
    ac.temperature = 10
    println(ac.temperature)
    ac.temperature = 35
    println(ac.temperature)
}
Output
18
16
30

Callers just write ac.temperature = 10, as if it were a plain variable, and the setter quietly keeps the value between 16 and 30. Writing temperature = value inside the setter would call the setter again forever; always assign to field. Note that the initial value 24 is stored directly and does not pass through the setter.

Default values and secondary constructors

Default values in the primary constructor usually replace multiple constructors. When you do need another way to create an object, add a secondary constructor with constructor, which must call the primary one with this(...).

Kotlin
class Product(val name: String, val price: Int, val category: String = "General") {
    constructor(name: String) : this(name, 0, "Free sample")

    fun label() = "$name [$category] Rs $price"
}

fun main() {
    println(Product("Pen", 20).label())
    println(Product("Notebook", 45, "Stationery").label())
    println(Product(name = "Mug", category = "Kitchen", price = 199).label())
    println(Product("Sticker").label())
}
Output
Pen [General] Rs 20
Notebook [Stationery] Rs 45
Mug [Kitchen] Rs 199
Sticker [Free sample] Rs 0

Named arguments work with constructors too, so the third call can list the values in any order.

Visibility modifiers

Kotlin has four visibility levels. Everything is public by default, so you only write a modifier to hide something.

ModifierVisible to
public (default)everyone
internalcode in the same module (for example, one Gradle project)
protectedthe class and its subclasses
privatethe class only (or the file, for top-level declarations)

A good habit is to make properties private or give them a private set unless other code really needs to change them.

Explicit backing fields and lazy properties

A common need is a list that the class can change but other code can only read. Kotlin 2.4 makes this short with an explicit backing field: the property has a read-only public type, and the field declaration inside it holds a mutable value.

Kotlin
class Classroom(val name: String) {
    val students: List<String>
        field = mutableListOf()

    fun join(student: String) {
        if (student !in students) students.add(student)
    }
}

fun main() {
    val room = Classroom("10-B")
    room.join("Aarav")
    room.join("Meera")
    room.join("Aarav")
    println(room.students)
    // room.students.add("Kabir")   // does not compile outside the class
}
Output
[Aarav, Meera]

Inside the class, students is treated as a MutableList<String>; outside, it is only a List<String>.

Two more tools help with properties that cannot be set in the constructor:

  • lateinit var adapter: ListAdapter promises the value will be assigned before first use (common in Android). Reading it too early throws an exception.
  • val summary: String by lazy { ... } computes a value the first time it is read and then remembers it, which is useful for expensive work.

Common mistakes

  • Writing new. new Student(...) does not compile; just call Student(...).
  • Forgetting val/var in the constructor. class Student(name: String) makes name a plain parameter, so student.name does not exist.
  • Comparing objects with ==. For ordinary classes, == is true only for the very same object. Two Student objects with the same data are not equal unless you write equals or use a data class.
  • Public mutable state. A public var balance lets any code set any value. Use private set and methods that check the rules.

Key points

  • class Name(val a: Type, var b: Type) declares a class with properties in one line; create objects without new.
  • init blocks run during creation; require rejects invalid data.
  • Custom getters compute values; custom setters use field to store them.
  • private set allows reading from outside but changes only inside the class.
  • Default arguments replace most extra constructors; secondary constructors call this(...).
  • Kotlin 2.4 explicit backing fields expose a read-only type over a mutable field.

Exercise

Create a class LibraryBook(val title: String, val author: String) with a property issuedTo: String? that starts as null and has a private setter. Add issue(member: String): Boolean (fails if already issued), giveBack(), and a custom getter isAvailable. Override toString to show the title and either "available" or the member's name. Test it with Meera and Kabir.

Show solution
Kotlin
class LibraryBook(val title: String, val author: String) {
    var issuedTo: String? = null
        private set

    val isAvailable: Boolean
        get() = issuedTo == null

    fun issue(member: String): Boolean {
        if (!isAvailable) return false
        issuedTo = member
        return true
    }

    fun giveBack() {
        issuedTo = null
    }

    override fun toString() = "$title: " + (issuedTo ?: "available")
}

fun main() {
    val book = LibraryBook("Wings of Fire", "A. P. J. Abdul Kalam")
    println(book.issue("Meera"))
    println(book.issue("Kabir"))
    println(book)
    book.giveBack()
    println(book.isAvailable)
    println(book)
}
Output
true
false
Wings of Fire: Meera
true
Wings of Fire: available

The private setter means only issue and giveBack can change who has the book, so the second issue call is refused while Meera has it.

Quick Quiz

1. How do you create a Student object in Kotlin?

2. In class Box(size: Int), what is size?

3. What does private set on a property do?

4. Inside a custom setter, how do you store the new value?

Learn Kotlin with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call