Kelas

Kelas #

Kelas adalah cetak biru untuk membuat objek — ia mendefinisikan data apa yang disimpan (properti) dan apa yang bisa dilakukan (metode). Di Kotlin, kelas dirancang lebih ringkas dari Java: konstruktor bisa langsung di header kelas, properti bisa sekaligus dideklarasikan di sana, dan banyak boilerplate seperti equals(), hashCode(), dan toString() bisa di-generate otomatis dengan data class. Artikel ini membahas semua bentuk kelas yang tersedia di Kotlin secara mendalam — dari kelas biasa, kelas data, sealed class, object, hingga pola-pola penting seperti inheritance dan visibilitas.

Mendefinisikan Kelas #

Kelas paling sederhana di Kotlin hanya butuh satu kata kunci:

class KelasKosong  // tidak butuh kurung kurawal jika tidak ada body

Kelas yang lebih berguna punya properti dan metode:

class Mobil(val merek: String, val model: String, var tahun: Int) {

    // Properti tambahan dengan nilai default
    var odometer: Double = 0.0
    var warna: String = "Putih"

    // Metode
    fun jalan(jarakKm: Double) {
        odometer += jarakKm
        println("$merek $model melaju $jarakKm km. Total: $odometer km")
    }

    fun info(): String = "$merek $model ($tahun) — odometer: ${odometer}km"

    override fun toString() = info()
}

val mobil = Mobil("Toyota", "Avanza", 2022)
mobil.warna = "Merah"
mobil.jalan(150.0)
mobil.jalan(75.5)
println(mobil)  // Toyota Avanza (2022) — odometer: 225.5km

Perhatikan: di Kotlin tidak perlu kata kunci new untuk membuat objek — cukup panggil nama kelas seperti fungsi.


Konstruktor #

Primary Constructor #

Primary constructor ditulis langsung di header kelas, setelah nama kelas. Parameter yang ditandai val atau var otomatis menjadi properti kelas.

class Pengguna(
    val id: Long,
    val nama: String,
    val email: String,
    var aktif: Boolean = true
)

val pengguna = Pengguna(1L, "Budi Santoso", "[email protected]")
println(pengguna.nama)   // Budi Santoso
println(pengguna.aktif)  // true

Parameter tanpa val/var hanya tersedia selama inisialisasi — tidak menjadi properti:

class Lingkaran(jariJari: Double) {
    // jariJari hanya tersedia di sini, bukan sebagai properti
    val luas = Math.PI * jariJari * jariJari
    val keliling = 2 * Math.PI * jariJari
}

val lingkaran = Lingkaran(5.0)
println(lingkaran.luas)      // 78.53...
// lingkaran.jariJari        // ✗ error — bukan properti

Blok init #

Blok init dieksekusi tepat setelah primary constructor. Bisa ada lebih dari satu blok init — semuanya dieksekusi secara berurutan.

class Akun(val username: String, val email: String) {

    val usernameNormal: String

    init {
        // Validasi saat konstruksi
        require(username.length >= 3) { "Username minimal 3 karakter" }
        require(email.contains("@")) { "Format email tidak valid" }

        usernameNormal = username.lowercase().trim()
        println("Akun '$usernameNormal' berhasil dibuat")
    }
}

val akun = Akun("Budi", "[email protected]")  // "Akun 'budi' berhasil dibuat"
// val invalid = Akun("ab", "bukan-email") // ✗ IllegalArgumentException

Secondary Constructor #

Secondary constructor dideklarasikan dengan kata kunci constructor di dalam body kelas. Ia harus mendelegasikan ke primary constructor (langsung atau tidak langsung) menggunakan this(...).

class Titik(val x: Double, val y: Double) {

    // Secondary constructor: buat Titik dari koordinat integer
    constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble())

    // Secondary constructor: buat Titik di origin
    constructor() : this(0.0, 0.0)

    fun jarakKe(lain: Titik): Double {
        val dx = x - lain.x
        val dy = y - lain.y
        return Math.sqrt(dx * dx + dy * dy)
    }

    override fun toString() = "($x, $y)"
}

val p1 = Titik(3.0, 4.0)
val p2 = Titik(0, 0)         // secondary constructor
val p3 = Titik()             // secondary constructor
println(p1.jarakKe(p2))      // 5.0
Dalam praktik sehari-hari, secondary constructor jarang dibutuhkan di Kotlin karena default parameter sudah menangani sebagian besar kasus. Gunakan secondary constructor hanya jika perlu logika yang berbeda secara fundamental antar cara konstruksi.

Properti dan Getter/Setter Kustom #

Properti di Kotlin lebih dari sekadar field — mereka bisa punya getter dan setter kustom yang dieksekusi setiap kali properti diakses atau dimodifikasi.

class Suhu(private var _celsius: Double) {

    // Properti dengan getter dan setter kustom
    var celsius: Double
        get() = _celsius
        set(value) {
            require(value >= -273.15) { "Suhu tidak boleh di bawah nol mutlak" }
            _celsius = value
        }

    // Properti computed — dihitung dari properti lain
    val fahrenheit: Double
        get() = celsius * 9.0 / 5.0 + 32

    val kelvin: Double
        get() = celsius + 273.15

    val statusSuhu: String
        get() = when {
            celsius < 0   -> "Beku"
            celsius < 20  -> "Dingin"
            celsius < 30  -> "Nyaman"
            celsius < 37  -> "Hangat"
            else          -> "Panas"
        }
}

val suhu = Suhu(25.0)
println(suhu.celsius)      // 25.0
println(suhu.fahrenheit)   // 77.0
println(suhu.kelvin)       // 298.15
println(suhu.statusSuhu)   // Nyaman

suhu.celsius = 100.0
println(suhu.fahrenheit)   // 212.0
// suhu.celsius = -300.0   // ✗ IllegalArgumentException

field — Backing Field #

Di dalam getter atau setter, gunakan field (bukan nama propertinya) untuk mengakses nilai aktual yang tersimpan. Ini menghindari rekursi tak terbatas:

class Skor {
    var nilai: Int = 0
        set(value) {
            field = if (value < 0) 0 else value  // gunakan 'field', bukan 'nilai'
        }
}

val skor = Skor()
skor.nilai = 100
println(skor.nilai)  // 100
skor.nilai = -50
println(skor.nilai)  // 0 — dikoreksi ke minimum

Visibilitas #

Kotlin memiliki empat modifier visibilitas:

ModifierBisa diakses dari
public (default)Mana saja
privateDi dalam kelas yang sama saja
protectedDi dalam kelas yang sama dan subclass
internalDi dalam modul yang sama
class RekeningBank(private val nomorRekening: String) {

    private var _saldo: Double = 0.0

    val saldo: Double        // read-only dari luar
        get() = _saldo

    internal fun auditInternal(): String = "Rekening $nomorRekening: $_saldo"

    fun setor(jumlah: Double) {
        require(jumlah > 0) { "Jumlah setoran harus positif" }
        _saldo += jumlah
        catatTransaksi("SETOR", jumlah)  // private — hanya bisa dari dalam
    }

    fun tarik(jumlah: Double) {
        require(jumlah > 0) { "Jumlah penarikan harus positif" }
        require(_saldo >= jumlah) { "Saldo tidak mencukupi" }
        _saldo -= jumlah
        catatTransaksi("TARIK", jumlah)
    }

    private fun catatTransaksi(tipe: String, jumlah: Double) {
        println("[LOG] $tipe Rp${"%,.0f".format(jumlah)} | Saldo: Rp${"%,.0f".format(_saldo)}")
    }
}

val rekening = RekeningBank("1234567890")
rekening.setor(1_000_000.0)
rekening.tarik(250_000.0)
println("Saldo: Rp${"%,.0f".format(rekening.saldo)}")
// rekening._saldo   // ✗ error — private
// rekening.nomorRekening  // ✗ error — private

Inheritance — Pewarisan #

Di Kotlin, semua kelas final secara default — tidak bisa diwarisi. Untuk memperbolehkan pewarisan, tandai kelas dengan open. Begitu juga dengan metode yang ingin bisa di-override.

open class Hewan(val nama: String) {

    open fun bersuara(): String = "..."

    open fun deskripsikan(): String = "$nama bersuara: ${bersuara()}"

    // Metode final — tidak bisa di-override
    fun tidur() = println("$nama sedang tidur...")
}

class Kucing(nama: String) : Hewan(nama) {
    override fun bersuara() = "Meow!"
}

class Anjing(nama: String, val ras: String) : Hewan(nama) {
    override fun bersuara() = "Woof!"

    override fun deskripsikan() = "${super.deskripsikan()} (Ras: $ras)"
}

class BurungBeo(nama: String, private val kataan: String) : Hewan(nama) {
    override fun bersuara() = kataan
}

val hewan = listOf(
    Kucing("Mimi"),
    Anjing("Rex", "German Shepherd"),
    BurungBeo("Polly", "Halo! Siapa kamu?")
)

hewan.forEach { println(it.deskripsikan()) }
// Mimi bersuara: Meow!
// Rex bersuara: Woof! (Ras: German Shepherd)
// Polly bersuara: Halo! Siapa kamu?

Mencegah Override Lebih Lanjut #

Gunakan final pada override untuk menghentikan rantai override:

open class Bentuk {
    open fun gambar() = println("Menggambar bentuk")
}

open class Segitiga : Bentuk() {
    final override fun gambar() = println("Menggambar segitiga")
    // Kelas turunan dari Segitiga tidak bisa override gambar() lagi
}

Kelas Abstrak #

Kelas abstrak tidak bisa diinstansiasi secara langsung. Ia mendefinisikan kontrak berupa method abstrak yang wajib diimplementasikan oleh subclass.

abstract class Laporan(val judul: String) {

    // Method abstrak — wajib diimplementasikan subclass
    abstract fun buatKonten(): String
    abstract fun format(): String

    // Method konkret — sudah punya implementasi, bisa di-override
    open fun header(): String = "=== $judul ===\n"

    // Method final — tidak bisa di-override
    fun cetak() {
        println(header())
        println(buatKonten())
        println("\nFormat: ${format()}")
    }
}

class LaporanPenjualan(judul: String, private val totalPenjualan: Double) : Laporan(judul) {
    override fun buatKonten() = "Total Penjualan: Rp${"%,.0f".format(totalPenjualan)}"
    override fun format() = "PDF"
}

class LaporanStok(judul: String, private val daftarProduk: Map<String, Int>) : Laporan(judul) {
    override fun buatKonten(): String {
        return daftarProduk.entries.joinToString("\n") { (produk, stok) ->
            "  - $produk: $stok unit"
        }
    }
    override fun format() = "Excel"
    override fun header() = "📊 ${super.header()}"
}

val laporan = LaporanPenjualan("Q1 2024", 125_000_000.0)
laporan.cetak()

val stok = LaporanStok("Inventaris Maret", mapOf("Laptop" to 15, "Mouse" to 42, "Keyboard" to 28))
stok.cetak()

Data Class #

data class adalah kelas yang tugasnya menyimpan data. Kotlin otomatis menghasilkan toString(), equals(), hashCode(), dan copy() berdasarkan properti di primary constructor.

data class Produk(
    val id: Long,
    val nama: String,
    val harga: Double,
    val kategori: String,
    val stok: Int = 0
)

val laptop = Produk(1L, "Laptop Gaming", 15_000_000.0, "Elektronik", 10)
val mouse  = Produk(2L, "Mouse Wireless", 250_000.0, "Elektronik", 50)

// toString() otomatis
println(laptop)
// Produk(id=1, nama=Laptop Gaming, harga=1.5E7, kategori=Elektronik, stok=10)

// equals() membandingkan nilai, bukan referensi
val laptop2 = Produk(1L, "Laptop Gaming", 15_000_000.0, "Elektronik", 10)
println(laptop == laptop2)   // true

// copy() buat salinan dengan sebagian nilai berbeda
val laptopDiskon = laptop.copy(harga = 13_500_000.0)
println(laptopDiskon.harga)  // 1.35E7

// Destructuring
val (id, nama, harga) = laptop
println("$id: $nama — Rp${"%,.0f".format(harga)}")

Data Class vs Kelas Biasa #

GUNAKAN data class jika:
  ✓ Kelas berfungsi sebagai wadah data (DTO, model, respons API)
  ✓ Butuh equals() dan hashCode() berdasarkan nilai
  ✓ Butuh copy() untuk membuat versi yang sedikit berbeda
  ✓ Butuh destructuring

GUNAKAN kelas biasa jika:
  ✓ Kelas punya logika bisnis yang signifikan
  ✓ Perlu mengontrol equals()/hashCode() secara kustom
  ✓ Kelas didesain untuk diwarisi (data class tidak bisa open)
  ✓ Identitas objek lebih penting dari nilainya

Object — Singleton dan Anonymous Object #

Singleton dengan object #

object mendeklarasikan singleton — kelas yang hanya punya satu instance, dibuat saat pertama kali diakses.

object KonfigurasiAplikasi {
    const val VERSI = "2.1.0"
    const val NAMA_APP = "MyKotlinApp"
    var modeDev = false

    fun info() = "$NAMA_APP v$VERSI (dev=$modeDev)"

    fun muatDariEnv() {
        modeDev = System.getenv("APP_ENV") == "development"
    }
}

println(KonfigurasiAplikasi.info())
KonfigurasiAplikasi.muatDariEnv()

Anonymous Object #

Anonymous object berguna untuk membuat implementasi interface atau abstract class sekali pakai tanpa mendefinisikan kelas bernama:

interface KlikListener {
    fun onClick(sumber: String)
}

fun pasangKlik(listener: KlikListener) {
    listener.onClick("Tombol OK")
}

// Implementasi inline tanpa nama kelas
pasangKlik(object : KlikListener {
    override fun onClick(sumber: String) {
        println("Diklik: $sumber")
    }
})

Sealed Class #

Sealed class adalah hierarki kelas yang tertutup — semua subclass-nya harus dideklarasikan di file yang sama. Ini memungkinkan compiler untuk mengetahui semua kemungkinan subtype, sehingga when bisa melakukan exhaustive check.

sealed class HasilJaringan<out T> {
    data class Berhasil<T>(val data: T) : HasilJaringan<T>()
    data class Gagal(val pesan: String, val kode: Int = 0) : HasilJaringan<Nothing>()
    object Memuat : HasilJaringan<Nothing>()
    object TidakAdaKoneksi : HasilJaringan<Nothing>()
}

fun <T> tanganiHasil(hasil: HasilJaringan<T>): String {
    return when (hasil) {
        is HasilJaringan.Berhasil      -> "Data diterima: ${hasil.data}"
        is HasilJaringan.Gagal         -> "Error ${hasil.kode}: ${hasil.pesan}"
        is HasilJaringan.Memuat        -> "Sedang memuat..."
        is HasilJaringan.TidakAdaKoneksi -> "Periksa koneksi internetmu"
        // Tidak perlu else — compiler tahu semua kasus sudah ditangani
    }
}

println(tanganiHasil(HasilJaringan.Berhasil("respons API")))
println(tanganiHasil(HasilJaringan.Gagal("Server tidak tersedia", 503)))
println(tanganiHasil(HasilJaringan.Memuat))

Companion Object #

Companion object adalah singleton yang terikat ke sebuah kelas — setara dengan static di Java. Ia bisa punya nama, atau menggunakan nama default Companion.

class Token private constructor(val nilai: String, val expiry: Long) {

    companion object {
        private const val DURASI_VALID_MS = 3_600_000L  // 1 jam

        fun buat(userId: Long): Token {
            val nilai = "tok_${userId}_${System.currentTimeMillis()}"
            val expiry = System.currentTimeMillis() + DURASI_VALID_MS
            return Token(nilai, expiry)
        }

        fun dariString(raw: String): Token? {
            val bagian = raw.split("_")
            return if (bagian.size == 3) {
                Token(raw, System.currentTimeMillis() + DURASI_VALID_MS)
            } else null
        }
    }

    val masihValid: Boolean
        get() = System.currentTimeMillis() < expiry

    override fun toString() = "Token($nilai, valid=$masihValid)"
}

val token = Token.buat(42L)
println(token)
println(token.masihValid)
// val invalid = Token("abc", 0L)  // ✗ error — constructor private

Hierarki Kelas di Kotlin #

flowchart TD
    A["class biasa\n(final by default)"] --> B["open class\n(bisa diwarisi)"]
    B --> C["abstract class\n(tidak bisa diinstansiasi)"]
    D["data class\n(otomatis equals/hashCode/copy)"] --> E["Tidak bisa open\ntidak bisa abstract"]
    F["sealed class\n(subtype terbatas di file yang sama)"] --> G["Subtype bisa class,\ndata class, atau object"]
    H["object\n(singleton)"] --> I["companion object\n(terikat ke kelas)"]
    H --> J["anonymous object\n(implementasi sekali pakai)"]

Ringkasan #

  • Primary constructor di header — parameter bertanda val/var langsung jadi properti. Parameter tanpa tanda itu hanya tersedia selama inisialisasi di init.
  • Blok init untuk validasi — gunakan require() atau check() di init untuk memastikan objek selalu dalam keadaan valid saat dibuat.
  • Semua kelas final by default — tambahkan open untuk mengizinkan pewarisan, dan open pada setiap metode yang boleh di-override.
  • Getter/setter kustom dengan field — gunakan field di dalam getter/setter untuk merujuk ke nilai aktual properti, bukan nama properti itu sendiri (untuk menghindari rekursi).
  • data class untuk model data — dapatkan equals(), hashCode(), toString(), dan copy() secara gratis. Gunakan ini untuk DTO, respons API, dan model domain.
  • object untuk singleton — Kotlin menjamin satu instance per JVM session. Lebih aman dan lebih ringkas dari pola Singleton manual.
  • sealed class untuk tipe tertutup — semua subtype diketahui compiler, sehingga when bisa exhaustive. Ideal untuk hasil operasi (Berhasil/Gagal/Memuat) dan state mesin.
  • companion object menggantikan static — member companion object bisa dipanggil dengan nama kelas, bisa punya nama sendiri, dan bisa mengimplementasikan interface.
  • Visibilitas private secara agresif — sembunyikan implementasi detail dengan private. Ekspos hanya apa yang memang perlu diakses dari luar.

← Sebelumnya: Fungsi   Berikutnya: Interface →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact