In the previous tutorial, you learned about control flow. Now let’s learn about classes — the foundation of object-oriented programming in Kotlin.
Kotlin classes are more concise than Java classes. What takes 50 lines in Java takes 1 line in Kotlin with data classes.
In this tutorial, you will learn:
- Basic classes and properties
- Constructors (primary and secondary)
- Inheritance
- Abstract classes and interfaces
- Data classes
- Enum classes
- Sealed classes
- Objects and companion objects
Basic Class
A class in Kotlin has a primary constructor in the class header:
class Person(val name: String, var age: Int) {
fun introduce(): String {
return "Hi, I'm $name and I'm $age years old."
}
fun haveBirthday() {
age++
}
}
Usage:
val person = Person("Alex", 25)
println(person.introduce()) // Hi, I'm Alex and I'm 25 years old.
println(person.name) // Alex
person.haveBirthday()
println(person.age) // 26
Key points:
val name— read-only property (getter only)var age— read-write property (getter and setter)- No
newkeyword needed to create an instance
Properties
Custom Getters and Setters
class BankAccount(val owner: String, initialBalance: Double) {
var balance: Double = initialBalance
private set // Can only be changed inside this class
// Computed property — calculated every time you access it
val isOverdrawn: Boolean
get() = balance < 0
fun deposit(amount: Double) {
require(amount > 0) { "Deposit amount must be positive" }
balance += amount
}
fun withdraw(amount: Double): Boolean {
require(amount > 0) { "Withdrawal amount must be positive" }
return if (balance >= amount) {
balance -= amount
true
} else {
false
}
}
}
Usage:
val account = BankAccount("Sam", 1000.0)
account.deposit(500.0)
println(account.balance) // 1500.0
account.withdraw(200.0)
println(account.balance) // 1300.0
println(account.isOverdrawn) // false
// account.balance = 9999.0 // Error: setter is private
private set means only the class itself can change the value. Outside code can read it but not write it.
Init Block
The init block runs when the object is created:
class BankAccount(val owner: String, initialBalance: Double) {
init {
require(owner.isNotBlank()) { "Owner name cannot be blank" }
}
// ...
}
// This crashes with IllegalArgumentException
// val bad = BankAccount("", 100.0)
Constructors
Primary Constructor
The primary constructor is part of the class header:
class Person(val name: String, var age: Int)
Secondary Constructor
Secondary constructors provide alternative ways to create objects:
class Rectangle(val width: Double, val height: Double) {
// Secondary constructor — creates a square
constructor(side: Double) : this(side, side)
val area: Double get() = width * height
val isSquare: Boolean get() = width == height
}
Usage:
val rect = Rectangle(10.0, 5.0)
println(rect.area) // 50.0
println(rect.isSquare) // false
val square = Rectangle(7.0) // Uses secondary constructor
println(square.area) // 49.0
println(square.isSquare) // true
Secondary constructors must call the primary constructor with this(...).
Inheritance
In Kotlin, classes are final by default. You must use open to allow inheritance:
open class Animal(val name: String, val sound: String) {
open fun makeSound(): String {
return "$name says $sound"
}
fun describe(): String {
return "I am a $name"
}
}
class Dog(name: String) : Animal(name, "Woof") {
override fun makeSound(): String {
return "$name says Woof! Woof!"
}
fun fetch(item: String): String {
return "$name fetches the $item"
}
}
class Cat(name: String) : Animal(name, "Meow") {
override fun makeSound(): String {
return "$name says Meow~"
}
}
Usage:
val dog = Dog("Rex")
println(dog.makeSound()) // Rex says Woof! Woof!
println(dog.fetch("ball")) // Rex fetches the ball
println(dog.describe()) // I am a Rex
val cat = Cat("Luna")
println(cat.makeSound()) // Luna says Meow~
Rules:
open class— can be inheritedopen fun— can be overriddenoverride fun— overrides a parent function- The
: Animal(name, "Woof")syntax calls the parent constructor
Abstract Classes
Abstract classes cannot be instantiated. They define a contract that subclasses must follow:
abstract class Shape {
abstract fun area(): Double
abstract fun perimeter(): Double
// Non-abstract function — shared by all subclasses
fun description(): String {
return "${this::class.simpleName}: area=${"%.2f".format(area())}"
}
}
class Circle(val radius: Double) : Shape() {
override fun area() = Math.PI * radius * radius
override fun perimeter() = 2 * Math.PI * radius
}
class Square(val side: Double) : Shape() {
override fun area() = side * side
override fun perimeter() = 4 * side
}
Usage:
val circle = Circle(5.0)
println(circle.description()) // Circle: area=78.54
val square = Square(4.0)
println(square.description()) // Square: area=16.00
Interfaces
Interfaces define a contract. A class can implement multiple interfaces:
interface Printable {
fun print(): String
}
interface Loggable {
// Default implementation
fun log(): String {
return "[LOG] ${this::class.simpleName}"
}
}
class Report(val title: String, val content: String) : Printable, Loggable {
override fun print(): String {
return "=== $title ===\n$content"
}
// Uses default log() from Loggable
}
Usage:
val report = Report("Monthly Report", "Everything is going well.")
println(report.print()) // === Monthly Report === ...
println(report.log()) // [LOG] Report
Interfaces can have default implementations (like log() above). Classes can override them or use the defaults.
Data Classes
Data classes are Kotlin’s killer feature for holding data. One line replaces dozens of lines of Java:
data class User(
val name: String,
val age: Int,
val email: String
)
This single line generates:
equals()— compares by valueshashCode()— consistent with equalstoString()— readable output likeUser(name=Alex, age=25, email=alex@example.com)copy()— create a modified copycomponentN()— for destructuring
Using Data Classes
val user1 = User("Alex", 25, "alex@example.com")
val user2 = User("Alex", 25, "alex@example.com")
// equals — compares by values
println(user1 == user2) // true
// toString — readable
println(user1) // User(name=Alex, age=25, email=alex@example.com)
// copy — change some fields
val user3 = user1.copy(name = "Sam", age = 30)
println(user3) // User(name=Sam, age=30, email=alex@example.com)
// Destructuring
val (name, age, email) = user1
println("$name, $age, $email") // Alex, 25, alex@example.com
copy() is especially useful when working with immutable data. Instead of changing an object, you create a new one with the changes you want.
Enum Classes
Enums represent a fixed set of values:
enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF"),
WHITE("#FFFFFF"),
BLACK("#000000");
fun isDark(): Boolean = this == BLACK || this == BLUE
}
enum class Direction {
NORTH, SOUTH, EAST, WEST;
fun opposite(): Direction = when (this) {
NORTH -> SOUTH
SOUTH -> NORTH
EAST -> WEST
WEST -> EAST
}
}
Usage:
println(Color.RED.hex) // #FF0000
println(Color.BLACK.isDark()) // true
println(Direction.NORTH.opposite()) // SOUTH
Enums can have properties, methods, and implement interfaces.
Sealed Classes
Sealed classes represent restricted hierarchies. All subclasses must be in the same file:
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String, val code: Int) : Result()
data object Loading : Result()
}
The key advantage: when with sealed classes doesn’t need an else branch because the compiler knows all possible subclasses:
fun handleResult(result: Result): String {
return when (result) {
is Result.Success -> "Success: ${result.data}"
is Result.Error -> "Error ${result.code}: ${result.message}"
is Result.Loading -> "Loading..."
// No else needed!
}
}
If you add a new subclass to the sealed class, the compiler forces you to handle it in every when expression. This prevents bugs.
Object (Singleton)
object creates a singleton — a class with exactly one instance:
object AppConfig {
const val APP_NAME = "Kotlin Tutorial"
const val VERSION = "1.0"
var isDebug = false
fun info(): String = "$APP_NAME v$VERSION (debug=$isDebug)"
}
// Usage — no need to create an instance
println(AppConfig.info()) // Kotlin Tutorial v1.0 (debug=false)
AppConfig.isDebug = true
println(AppConfig.info()) // Kotlin Tutorial v1.0 (debug=true)
Companion Object
Companion objects are like static members in Java:
class DatabaseConnection private constructor(val url: String) {
companion object {
private var instance: DatabaseConnection? = null
fun create(url: String): DatabaseConnection {
if (instance == null) {
instance = DatabaseConnection(url)
}
return instance!!
}
}
}
// Usage
val db = DatabaseConnection.create("jdbc:postgresql://localhost/mydb")
The private constructor means you can only create instances through the companion object’s create function.
Practical Example: Task Manager
Here is a complete example showing classes working together:
data class Task(
val id: Int,
val title: String,
val isCompleted: Boolean = false
)
class TaskManager {
private val tasks = mutableListOf<Task>()
private var nextId = 1
fun addTask(title: String): Task {
val task = Task(id = nextId++, title = title)
tasks.add(task)
return task
}
fun completeTask(id: Int): Boolean {
val index = tasks.indexOfFirst { it.id == id }
if (index == -1) return false
tasks[index] = tasks[index].copy(isCompleted = true)
return true
}
fun getPendingTasks(): List<Task> = tasks.filter { !it.isCompleted }
fun getCompletedTasks(): List<Task> = tasks.filter { it.isCompleted }
}
Usage:
val manager = TaskManager()
manager.addTask("Learn Kotlin basics")
manager.addTask("Write unit tests")
manager.addTask("Build a project")
manager.completeTask(1)
println(manager.getPendingTasks())
// [Task(id=2, title=Write unit tests, isCompleted=false),
// Task(id=3, title=Build a project, isCompleted=false)]
println(manager.getCompletedTasks())
// [Task(id=1, title=Learn Kotlin basics, isCompleted=true)]
Notice how copy(isCompleted = true) creates a new Task instead of modifying the existing one. This is the immutable pattern — safe and predictable.
Summary
| Feature | Description |
|---|---|
class | Basic class (final by default) |
open class | Allows inheritance |
abstract class | Cannot be instantiated |
data class | Auto-generates equals, hashCode, toString, copy |
sealed class | Restricted hierarchy |
enum class | Fixed set of values |
object | Singleton |
companion object | Static-like members |
interface | Contract with optional defaults |
Source Code
You can find the complete source code for this tutorial on GitHub:
What’s Next?
In the next tutorial, Kotlin Tutorial #8: Collections — List, Set, Map, and Operations, you will learn:
- List, MutableList, Set, Map
- Collection operations: filter, map, flatMap
- Grouping, sorting, and reducing
- Practical collection patterns
Collections are used everywhere in Kotlin. They are essential for real-world programming.
Want to see Kotlin classes in action? Check out the Jetpack Compose Tutorial where you build real Android UIs with Kotlin.
This is part 7 of the Kotlin Tutorial series. Check out Part 6: Control Flow if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.