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:
| Modifier | Bisa diakses dari |
|---|---|
public (default) | Mana saja |
private | Di dalam kelas yang sama saja |
protected | Di dalam kelas yang sama dan subclass |
internal | Di 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/varlangsung jadi properti. Parameter tanpa tanda itu hanya tersedia selama inisialisasi diinit.- Blok
inituntuk validasi — gunakanrequire()ataucheck()diinituntuk memastikan objek selalu dalam keadaan valid saat dibuat.- Semua kelas
finalby default — tambahkanopenuntuk mengizinkan pewarisan, danopenpada setiap metode yang boleh di-override.- Getter/setter kustom dengan
field— gunakanfielddi dalam getter/setter untuk merujuk ke nilai aktual properti, bukan nama properti itu sendiri (untuk menghindari rekursi).data classuntuk model data — dapatkanequals(),hashCode(),toString(), dancopy()secara gratis. Gunakan ini untuk DTO, respons API, dan model domain.objectuntuk singleton — Kotlin menjamin satu instance per JVM session. Lebih aman dan lebih ringkas dari pola Singleton manual.sealed classuntuk tipe tertutup — semua subtype diketahui compiler, sehinggawhenbisa exhaustive. Ideal untuk hasil operasi (Berhasil/Gagal/Memuat) dan state mesin.companion objectmenggantikanstatic— member companion object bisa dipanggil dengan nama kelas, bisa punya nama sendiri, dan bisa mengimplementasikan interface.- Visibilitas
privatesecara agresif — sembunyikan implementasi detail denganprivate. Ekspos hanya apa yang memang perlu diakses dari luar.