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", then30. - Any primary constructor parameter marked
val/varis assigned to its property immediately. - Property initializers and
initblocks 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
valfor constructor properties by default; usevaronly for state that genuinely needs to change after construction. - Always add
val/varto a constructor parameter you intend to expose as a property — a bare parameter is invisible outside the constructor andinitblocks. - 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
initblocks small — use them for validation or logging tied to construction, not for general logic that belongs in a method. - Mark properties
privateunless they genuinely need to be part of the class’s public API. - Remember a class is
finalby default; only addopenwhen you deliberately intend it to be subclassed. - Don’t hand-write
equals(),hashCode(), andtoString()for simple value holders — use adata classinstead.
Practice Exercises
- Write a class
Carwithval make: String,val model: String, andvar mileage: Int. Add a methoddrive(distance: Int)that increasesmileage. Create a car, print its mileage, calldrive(50), and print the mileage again. - Write a class
Temperaturewhose 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 twoCounterinstances with different starting values, increment one of them by 3, and write down what you expectprintlnto show before running it.
Summary
- A class is declared with
class; its primary constructor can be written directly in the class header. val/varon a constructor parameter makes it a property with a generated getter (and setter forvar); without either, it is just a constructor parameter.- Property initializers and
initblocks 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 viathis(...). - Kotlin classes and members are
publicandfinalby default — useopento allow subclassing. - Plain classes get reference-based
==unless they overrideequals();===always checks reference identity. vallocks the reference, not the mutability of the object it points to.
