Classes & Constructors
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.
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
}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.
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
}false true Rohan: Rs 6000 false
There is a lot in this class, so take it piece by piece:
initialBalancehas noval, so it is just a constructor parameter, used to set upbalance, and not stored separately.private setmeans anyone can readbalance, but only code inside the class can change it. Money moves only throughdepositandwithdraw, which check the rules. This idea of protecting data is called encapsulation.require(condition) { message }throws anIllegalArgumentExceptionwith your message if the condition is false, so an account with a negative balance can never exist.isPremiumhas a custom getter: it has no stored value and is calculated every time you read it.toStringdecides whatprintln(account)shows. Without it, you would see the class name and a code such asBankAccount@1b6d3586. Theoverridekeyword 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).
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)
}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(...).
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())
}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.
| Modifier | Visible to |
|---|---|
public (default) | everyone |
internal | code in the same module (for example, one Gradle project) |
protected | the class and its subclasses |
private | the 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.
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
}[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: ListAdapterpromises 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 callStudent(...). - Forgetting
val/varin the constructor.class Student(name: String)makesnamea plain parameter, sostudent.namedoes not exist. - Comparing objects with
==. For ordinary classes,==istrueonly for the very same object. TwoStudentobjects with the same data are not equal unless you writeequalsor use a data class. - Public mutable state. A public
var balancelets any code set any value. Useprivate setand 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 withoutnew.initblocks run during creation;requirerejects invalid data.- Custom getters compute values; custom setters use
fieldto store them. private setallows 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
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)
}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?
Kotlin has no new keyword; you call the constructor directly.
2. In class Box(size: Int), what is size?
Without val or var, a constructor parameter is not stored as a property.
3. What does private set on a property do?
The getter stays public; only the setter becomes private.
4. Inside a custom setter, how do you store the new value?
field is the backing field; assigning to the property itself would call the setter again.
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
