Learn Kotlin - Classes, Objects & OOP
Episode 4 of 23

Learn Kotlin - Classes, Objects & OOP

This episode masters object-oriented programming in Kotlin: class declarations, constructors, properties, visibility modifiers, data classes and sealed classes, and object declarations and companion objects.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

You already have the basic syntax in hand. Now episode 4 builds the most important capability in application development: Object-Oriented Programming with Kotlin. You'll learn to declare classes, create constructors and properties, manage visibility, and use modern features like data classes and sealed classes.

Kotlin simplifies much of what's verbose in Java: the primary constructor sits directly in the class declaration, properties automatically generate getters and setters, and the object keyword eliminates the hand-written singleton pattern.

By the end of this episode you'll be able to design a clean, expressive domain model — a skill used directly in episodes 5 through 8.

Classes, Constructors, and Properties

Primary Constructors and Properties

Kotlin combines the class declaration, constructor, and properties in one block. The primary constructor is written directly in the class header:

KotlinClass with a primary constructor
class Produk(
    val nama: String,
    var harga: Int,
    val stok: Int = 0,
) {
    fun info(): String = "$nama seharga $harga"
}

The parameters val nama and var harga automatically become properties with getters (and a setter for var). stok has a default value of 0. To use the class, just write Produk("Kopi", 15000). No more verbose setNama methods. You can test this class with kotlinc Main.kt -include-runtime -d main.jar and then run the JAR.

Init Blocks and Secondary Constructors

Additional initialization logic goes in the init block, which executes when the object is created. A secondary constructor is used only when you need parameter variations that can't be expressed with default arguments:

KotlinInit block
class Rekening(val pemilik: String) {
    var saldo: Int = 0
    init {
        println("Rekening untuk $pemilik dibuat")
    }
    fun deposit(nominal: Int) {
        saldo += nominal
    }
}

The init block runs after the properties are initialized. For the vast majority of cases, default arguments are enough — secondary constructors are rarely needed in Kotlin.

Visibility Modifiers and Encapsulation

Kotlin provides four visibility modifiers: private, protected, internal, and public (the default). What distinguishes it from Java is internal, which restricts access to within the same module — useful for hiding a library's internal API:

KotlinInternal and private
internal class ServiceAktif {
    private val token = "rahasia"
    fun akses(): String = token
}

The private modifier on the token property ensures only the class itself can access it. internal makes the class visible only within the same module, a common pattern in libraries that expose a clean public API.

Data Classes and Sealed Classes

Data Class: The Perfect Value Holder

data class is designed for data models: the compiler automatically generates equals, hashCode, toString, and copy based on its primary properties:

KotlinData class
data class Pengguna(val id: Int, val nama: String)
 
val a = Pengguna(1, "Budi")
val b = a.copy(nama = "Andi")
println(a) // Pengguna(id=1, nama=Budi)
println(a == Pengguna(1, "Budi")) // true

The copy method lets you create a copy with some values changed — very useful for immutable data. The automatically generated toString makes debugging data enjoyable.

Sealed Class for Closed Hierarchies

sealed class restricts subtypes to the same file, making it perfect for representing known states or results. Combined with when, it produces processing that the compiler guarantees to be complete:

KotlinSealed class for states
sealed class Status {
    data object Loading : Status()
    data class Sukses(val data: String) : Status()
    data class Gagal(val pesan: String) : Status()
}
 
fun tampilkan(s: Status): String = when (s) {
    is Status.Loading -> "Memuat..."
    is Status.Sukses -> "Berhasil: ${s.data}"
    is Status.Gagal -> "Gagal: ${s.pesan}"
}

Because the Status hierarchy is closed, the compiler knows all the possibilities — if you ever add a new subtype, compilation will fail in the when until you handle it. This is one of Kotlin's favorite safety features.

Inheritance and Interfaces

Classes and Methods Open for Inheritance

All Kotlin classes are final by default — inheritance must be requested explicitly with open. This is a design decision that encourages composition and prevents accidental inheritance:

KotlinInheritance with open
open class Hewan(val nama: String) {
    open fun suara(): String = "Hewan bersuara"
}
 
class Kucing(nama: String) : Hewan(nama) {
    override fun suara(): String = "Meow"
}

open on the Hewan class and the suara method allows inheritance and overriding. override must be written on any method that overrides a parent implementation — the compiler enforces this discipline.

Interfaces and Delegation

Interfaces in Kotlin can have default implementations on methods. More interestingly, Kotlin supports class delegation with the by keyword — implementing the composition pattern without writing boilerplate methods:

KotlinDelegation with by
interface Logger {
    fun log(pesan: String)
}
 
class ConsoleLogger : Logger {
    override fun log(pesan: String) = println(pesan)
}
 
class Service(private val logger: Logger) : Logger by logger

Service delegates all Logger methods to the injected logger instance. You don't need to reimplement anything — by handles the delegation. This pattern is used for clean dependency injection in episode 11.

Object Declarations and Companion Objects

Singletons with object

The object keyword declares a singleton — a single instance created automatically on first access:

KotlinSingleton with object
object Konfigurasi {
    val versi = "2.1"
    fun info() = "Versi $versi"
}

Konfigurasi.versi and Konfigurasi.info() are called directly without creating an instance. The object keyword removes all the singleton boilerplate: private constructor, static instance, and getter — all handled by the compiler.

Companion Object: Safe Static Members

companion object attaches to a class and resembles static members in Java, but can still use the associated class's type:

KotlinCompanion object
class Kalkulator {
    companion object {
        fun tambah(a: Int, b: Int): Int = a + b
    }
}
 
val hasil = Kalkulator.tambah(3, 4)

Kalkulator.tambah is called without an instance. Companion objects are useful for factory methods and class-bound constants — a pattern you'll see in many Kotlin libraries.

Closing

Episode 4 equips you with modern OOP the Kotlin way: classes with primary constructors, automatic properties, visibility modifiers including internal, data classes for data models, sealed classes for closed hierarchies, inheritance with open, interfaces with delegation, and object and companion objects for the singleton pattern.

The key takeaways:

  • Primary constructors and properties are declared directly in the class header.
  • Default arguments are usually enough; secondary constructors are rarely needed.
  • data class generates equals, hashCode, toString, and copy automatically.
  • sealed class guarantees that when handles every possible subtype.
  • Classes are final by default; use open to enable inheritance.
  • object creates singletons; companion object provides static members.

In episode 5 we'll discuss collections and the standard library — List, Set, and Map, collection operations like map, filter, fold, and flatMap, sequences with lazy evaluation, and string manipulation and utility functions that make data processing in Kotlin very concise.

Learn Kotlin - Classes, Objects & OOP | Learn Kotlin