Classes in Kotlin

A class in Kotlin is a blueprint for creating objects that bundle state (properties) and behavior (functions) together. Compared to Java, Kotlin classes are far less verbose: you can declare a class’s constructor and its properties in a single line, and the compiler generates the getters, setters, and backing fields for you. This lesson covers everything you need to declare, construct, and use classes correctly.

Overview: How Classes Work in Kotlin

You declare a class with the class keyword followed by a name. Most Kotlin classes also declare a primary constructor directly in the class header, right after the class name: class Person(val name: String, var age: Int). That single line does three things at once — it defines the constructor’s parameters, and because each parameter is prefixed with val or var, it also declares name and age as properties of every Person instance.

This is a major difference from Java. In Java you write a constructor, private fields, and public getters/setters as four separate pieces of boilerplate. In Kotlin, the compiler generates a backing field and a getter for a val property (and a setter too, for var) automatically. If a constructor parameter is not marked val or var, it is just a plain parameter: it can be used inside init blocks and inside the class body, but it is not a property and cannot be accessed from outside the class as instance.parameterName.

A class body (inside { }) can contain additional properties, init blocks, and member functions. When an instance is created, Kotlin assigns the primary constructor’s parameters first, then runs each property initializer and each init block in the order they are written in the class body — they can be interleaved. This matters when one property’s initializer depends on a value set earlier in an init block, or vice versa.

A class can also declare one or more secondary constructors with the constructor keyword. Every secondary constructor must eventually delegate to the primary constructor using : this(...), either directly or through another secondary constructor. In idiomatic Kotlin, secondary constructors are used less often than in Java, because default parameter values and named arguments cover most of the cases that would otherwise require constructor overloading.

Two more facts matter for accuracy: Kotlin classes and their members are public and final by default. “Public” means any code that can see the class can use it, unless you mark it private, protected, or internal. “Final” means the class cannot be subclassed unless you explicitly mark it open (subclassing is covered in the inheritance lesson). This is the opposite default from Java, where classes are subclassable unless marked final.

Syntax

The general shape of a class declaration looks like this:

class ClassName(parameter1: Type1, val parameter2: Type2 = defaultValue) : SuperClass(args) {

    val property1: Type3 = initialValue

    init {
        // runs when an instance is created
    }

    constructor(parameter1: Type1) : this(parameter1, defaultValue) {
        // secondary constructor body
    }

    fun methodName(): ReturnType {
        // method body
    }
}
Part Meaning
class ClassName(...) Declares the class and its primary constructor’s parameter list.
val / var before a parameter Turns that constructor parameter into a read-only or mutable property. Omit both and it is just a constructor parameter.
= defaultValue An optional default argument, letting callers omit that parameter.
: SuperClass(args) Optional superclass call, used when this class extends an open class.
init { } A block that runs during object construction, in the order it appears among property initializers.
constructor(...) : this(...) A secondary constructor, which must delegate to the primary constructor.
fun methodName() A member function (method) available on every instance.

Examples

Example 1: A basic class with a primary constructor

class Person(val name: String, var age: Int) {
    fun greet(): String {
        return "Hi, I'm $name and I'm $age years old."
    }
}

fun main() {
    val person = Person("Ava", 30)
    println(person.greet())
    person.age += 1
    println("Next year: ${person.age}")
}

Output:

Hi, I'm Ava and I'm 30 years old.
Next year: 31

name is a val, so it can never be reassigned after construction. age is a var, so person.age += 1 is legal — it calls the compiler-generated setter. No manual getter/setter code was written for either property.

Example 2: A computed property and an init block

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

    init {
        println("Created a rectangle ${width}x${height}")
    }
}

fun main() {
    val rect = Rectangle(3.0, 4.0)
    println("Area: ${rect.area}")
}

Output:

Created a rectangle 3.0x4.0
Area: 12.0

area has no stored backing field — its custom get() recomputes width * height every time it’s read. The init block runs once, during construction, printing a message before main ever touches area.

Example 3: A secondary constructor and a private property

class BankAccount(val owner: String, private var balance: Double = 0.0) {
    constructor(owner: String) : this(owner, 0.0) {
        println("Opened a zero-balance account for $owner")
    }

    fun deposit(amount: Double) {
        balance += amount
    }

    fun currentBalance(): Double = balance
}

fun main() {
    val acct1 = BankAccount("Priya", 100.0)
    acct1.deposit(50.0)
    println("${acct1.owner}'s balance: ${acct1.currentBalance()}")

    val acct2 = BankAccount("Sam")
    acct2.deposit(20.0)
    println("${acct2.owner}'s balance: ${acct2.currentBalance()}")
}

Output:

Priya's balance: 150.0
Opened a zero-balance account for Sam
Sam's balance: 20.0

balance is private, so outside code cannot read or write it directly — it must go through deposit and currentBalance. acct2 is built with the secondary constructor, which delegates to the primary constructor (setting balance to 0.0) before running its own body, which is why the “Opened a zero-balance account” message prints only for acct2, and only after acct1‘s balance line has already been printed.

How It Works Step by Step

When you write Person("Ava", 30), here is what actually happens:

  • Kotlin evaluates the constructor arguments left to right: "Ava", then 30.
  • Any primary constructor parameter marked val/var is assigned to its property immediately.
  • Property initializers and init blocks in the class body run, in the exact order they are written — not necessarily all initializers first.
  • If the object was created through a secondary constructor, that constructor’s delegation (: this(...)) runs the primary constructor and all of the above first, and only then does the secondary constructor’s own { } body execute.
  • A fully constructed object reference is returned to the caller, and its methods and properties can now be used.

Common Mistakes

Mistake 1: Forgetting val/var on a constructor parameter

Without val or var, a constructor parameter is not a property — it only exists inside the constructor and any init block or function that can see it as a parameter.

class Point(x: Int, y: Int) {
    fun show() = println("($x, $y)")
}

fun main() {
    val p = Point(1, 2)
    p.show()
    println(p.x) // error: unresolved reference 'x'
}

This fails to compile because x was never declared as a property. The fix is to add val (or var, if it should be mutable):

class Point(val x: Int, val y: Int) {
    fun show() = println("($x, $y)")
}

fun main() {
    val p = Point(1, 2)
    p.show()
    println(p.x)
}

Output:

(1, 2)
1

Mistake 2: Assuming == compares values for every class

In Kotlin, == calls equals(), and === checks whether two references point to the exact same object. But a plain class that does not override equals() inherits the default implementation, which is reference equality — so == behaves just like === until you say otherwise.

class Coord(val x: Int, val y: Int)

fun main() {
    val a = Coord(1, 2)
    val b = Coord(1, 2)
    println(a == b)
    println(a === b)
    println(a == a)
}

Output:

false
false
true

Many beginners expect a == b to be true because a and b hold the same x and y values. It is false because Coord never overrode equals(). The fix is either to override equals()/hashCode() yourself, or — the idiomatic Kotlin solution for simple value holders — declare the class as a data class, which generates structural equality automatically (covered in the next lesson).

Mistake 3: Assuming val makes a collection property immutable

val only prevents the reference from being reassigned; it says nothing about the contents of a mutable object the reference points to.

class Basket(val items: MutableList<String> = mutableListOf())

fun main() {
    val basket = Basket()
    basket.items.add("Apples")
    basket.items.add("Bread")
    println(basket.items)
}

Output:

[Apples, Bread]

basket.items can never be pointed at a different list (basket.items = mutableListOf() would not compile), but the list it points to is still mutable, so add works fine. If you want a truly unchangeable collection property, expose it as List<String> (read-only view) or copy it defensively.

Best Practices

  • Prefer val for constructor properties by default; use var only for state that genuinely needs to change after construction.
  • Always add val/var to a constructor parameter you intend to expose as a property — a bare parameter is invisible outside the constructor and init blocks.
  • Reach for default parameter values and named arguments before reaching for a secondary constructor; it is more idiomatic and avoids Java-style constructor overloading.
  • Keep init blocks small — use them for validation or logging tied to construction, not for general logic that belongs in a method.
  • Mark properties private unless they genuinely need to be part of the class’s public API.
  • Remember a class is final by default; only add open when you deliberately intend it to be subclassed.
  • Don’t hand-write equals(), hashCode(), and toString() for simple value holders — use a data class instead.

Practice Exercises

  • Write a class Car with val make: String, val model: String, and var mileage: Int. Add a method drive(distance: Int) that increases mileage. Create a car, print its mileage, call drive(50), and print the mileage again.
  • Write a class Temperature whose primary constructor takes a Celsius value (val celsius: Double), plus a secondary constructor that accepts a Fahrenheit value and converts it to Celsius ((fahrenheit - 32) / 1.8) before delegating to the primary constructor. Test both ways of constructing it.
  • Predict-the-output: given class Counter(var count: Int = 0) { init { println("Counter created at $count") } }, create two Counter instances with different starting values, increment one of them by 3, and write down what you expect println to show before running it.

Summary

  • A class is declared with class; its primary constructor can be written directly in the class header.
  • val/var on a constructor parameter makes it a property with a generated getter (and setter for var); without either, it is just a constructor parameter.
  • Property initializers and init blocks run in the order they’re written, right after the primary constructor’s parameters are assigned.
  • Secondary constructors, declared with constructor, must delegate to the primary constructor via this(...).
  • Kotlin classes and members are public and final by default — use open to allow subclassing.
  • Plain classes get reference-based == unless they override equals(); === always checks reference identity.
  • val locks the reference, not the mutability of the object it points to.