Sintaks Utama #
Kotlin dirancang dengan filosofi yang jelas: kode yang baik harus mudah dibaca, aman dari error umum seperti NullPointerException, dan tidak memaksa developer menulis boilerplate yang tidak perlu. Sintaks Kotlin adalah wujud dari filosofi itu — ringkas tapi ekspresif, familiar bagi developer Java tapi jauh lebih modern. Artikel ini membahas seluruh elemen sintaks inti Kotlin yang akan kamu gunakan setiap hari, dari struktur program paling dasar hingga fitur-fitur yang membuat Kotlin unggul dibanding bahasa sejenis.
Struktur Program #
Setiap program Kotlin memiliki satu titik masuk: fungsi main. Dari sinilah eksekusi dimulai, dan memahami strukturnya adalah fondasi untuk segalanya.
fun main() {
println("Hello, World!")
}
Tiga kata kunci yang perlu kamu pahami sejak awal:
| Elemen | Penjelasan |
|---|---|
fun | Kata kunci untuk mendefinisikan fungsi |
main | Nama khusus yang dikenali JVM sebagai titik masuk program |
println() | Fungsi dari standard library untuk mencetak ke konsol dengan newline |
Kotlin juga mendukung fungsi main dengan parameter untuk menerima argumen dari command line:
fun main(args: Array<String>) {
if (args.isEmpty()) {
println("Tidak ada argumen yang diberikan.")
return
}
println("Argumen yang diterima: ${args.size}")
args.forEachIndexed { index, arg ->
println(" [$index] $arg")
}
}
Satu hal yang langsung terasa berbeda dari Java: tidak ada titik koma di akhir statement. Kotlin menggunakan newline sebagai pemisah statement secara otomatis. Kamu bisa menambahkan titik koma jika ingin menulis dua statement di satu baris, tapi ini bukan gaya yang direkomendasikan.
// ANTI-PATTERN: titik koma yang tidak perlu
val x = 10;
val y = 20;
println(x + y);
// BENAR: tanpa titik koma
val x = 10
val y = 20
println(x + y)
Variabel dan Deklarasi #
Kotlin memiliki dua kata kunci untuk mendeklarasikan variabel: val dan var. Perbedaannya fundamental dan mempengaruhi cara kamu berpikir tentang state dalam program.
val (value) — nilainya tidak bisa diubah setelah diinisialisasi. Ini bukan berarti nilainya harus diketahui saat kompilasi — hanya berarti referensinya tidak bisa di-reassign.
var (variable) — nilainya bisa diubah kapan saja.
// val: sekali set, tidak bisa diganti
val pi = 3.14159
val namaAplikasi = "KotlinApp"
// pi = 3.0 // ✗ error: val cannot be reassigned
// var: bisa berubah
var skor = 0
skor = 100
skor += 50
println(skor) // 150
Type Inference #
Kotlin memiliki type inference yang kuat — compiler bisa menebak tipe dari nilai yang diberikan. Kamu tidak perlu selalu menuliskan tipenya secara eksplisit.
// ANTI-PATTERN: menuliskan tipe padahal sudah jelas dari nilai
val nama: String = "Budi"
val umur: Int = 25
val aktif: Boolean = true
// BENAR: biarkan compiler yang menebak
val nama = "Budi" // String
val umur = 25 // Int
val aktif = true // Boolean
val suhu = 36.5 // Double
val nilai = 36.5f // Float (suffix f)
val populasi = 8_000_000_000L // Long (suffix L)
Tulis tipe secara eksplisit hanya ketika tipenya tidak jelas dari nilai, atau ketika kamu ingin tipe yang berbeda dari yang akan diinfer compiler:
// Perlu eksplisit: ingin Float bukan Double
val suhu: Float = 36.5
// Perlu eksplisit: variabel yang dideklarasi tanpa nilai awal
var hasil: Int
hasil = hitungSesuatu()
Prinsip: Prefer val over var
#
Gunakan val sebagai default. Beralih ke var hanya jika memang perlu mengubah nilainya. Ini bukan aturan kaku, tapi kebiasaan ini menghasilkan kode yang lebih mudah diprediksi dan aman dari bug akibat mutasi tidak terduga.
// ANTI-PATTERN: pakai var padahal nilainya tidak pernah berubah
var maksimum = 100
var pesan = "Selamat datang"
// BENAR: gunakan val
val maksimum = 100
val pesan = "Selamat datang"
Tipe Data #
Kotlin memiliki tipe data primitif yang semuanya direpresentasikan sebagai objek — tidak ada perbedaan antara primitive dan wrapper seperti di Java.
Tipe Numerik #
val byteVal: Byte = 127 // -128 sampai 127
val shortVal: Short = 32767 // -32768 sampai 32767
val intVal: Int = 2_147_483_647 // sekitar ±2.1 miliar
val longVal: Long = 9_223_372_036_854_775_807L
val floatVal: Float = 3.14f // presisi ~7 digit desimal
val doubleVal: Double = 3.141592653589793 // presisi ~15 digit desimal
Underscore (_) bisa digunakan di dalam literal angka untuk keterbacaan — compiler mengabaikannya:
val populasiDunia = 8_000_000_000L
val hexColor = 0xFF_AA_33
val binaryMask = 0b1010_1010
Tipe String #
String di Kotlin bisa menggunakan template expression langsung di dalamnya — tidak perlu concatenation manual.
val nama = "Rina"
val umur = 28
val kota = "Bandung"
// ANTI-PATTERN: concatenation manual
val perkenalan = "Nama saya " + nama + ", umur " + umur + " tahun, dari " + kota + "."
// BENAR: string template
val perkenalan = "Nama saya $nama, umur $umur tahun, dari $kota."
// Untuk ekspresi kompleks, gunakan ${...}
val info = "Panjang nama: ${nama.length} karakter"
val status = "Dewasa: ${if (umur >= 18) "Ya" else "Tidak"}"
Kotlin juga mendukung raw string (multiline string) dengan triple quote — berguna untuk template teks panjang:
val query = """
SELECT u.nama, u.email, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
ORDER BY o.total DESC
LIMIT 10
""".trimIndent()
.trimIndent() menghapus indentasi leading yang seragam agar output bersih.
Struktur Kontrol #
If — Ekspresi, Bukan Hanya Statement #
Di Kotlin, if adalah ekspresi yang mengembalikan nilai. Ini menghilangkan kebutuhan operator ternary (?: di Java/JavaScript).
val nilai = 75
// if sebagai statement (seperti bahasa lain)
if (nilai >= 70) {
println("Lulus")
} else {
println("Tidak lulus")
}
// BENAR: if sebagai ekspresi — lebih ringkas
val status = if (nilai >= 70) "Lulus" else "Tidak lulus"
println(status)
// Ekspresi if bisa multiline
val kategori = if (nilai >= 90) {
"A"
} else if (nilai >= 80) {
"B"
} else if (nilai >= 70) {
"C"
} else {
"D"
}
When — Pengganti Switch yang Jauh Lebih Kuat #
when di Kotlin jauh lebih fleksibel dari switch di Java. Ia juga bisa digunakan sebagai ekspresi.
val kode = 404
// when dasar
when (kode) {
200 -> println("OK")
201 -> println("Created")
400 -> println("Bad Request")
401, 403 -> println("Auth Error") // multiple value dalam satu branch
in 500..599 -> println("Server Error") // range
else -> println("Status tidak dikenal")
}
// when sebagai ekspresi
val pesan = when (kode) {
200 -> "Sukses"
404 -> "Tidak ditemukan"
in 500..599 -> "Error server"
else -> "Status lain: $kode"
}
when juga bisa digunakan tanpa argumen sebagai pengganti if-else chain yang panjang:
val suhu = 35.0
val kondisi = when {
suhu < 0 -> "Beku"
suhu < 15 -> "Dingin"
suhu < 25 -> "Sejuk"
suhu < 35 -> "Hangat"
else -> "Panas"
}
println(kondisi) // Panas
Perulangan #
Kotlin menyediakan tiga jenis loop: for, while, dan do-while.
// for dengan range
for (i in 1..5) {
print("$i ") // 1 2 3 4 5
}
// range eksklusif dengan until
for (i in 0 until 5) {
print("$i ") // 0 1 2 3 4
}
// step — lewati N langkah
for (i in 0..10 step 2) {
print("$i ") // 0 2 4 6 8 10
}
// mundur dengan downTo
for (i in 5 downTo 1) {
print("$i ") // 5 4 3 2 1
}
// iterasi koleksi
val buah = listOf("Mangga", "Apel", "Jeruk")
for (item in buah) {
println(item)
}
// dengan index
for ((index, item) in buah.withIndex()) {
println("$index: $item")
}
// while
var counter = 0
while (counter < 5) {
println(counter)
counter++
}
// do-while: blok dieksekusi minimal sekali
var input: String
do {
input = bacaInput()
} while (input.isBlank())
Fungsi #
Fungsi didefinisikan dengan kata kunci fun. Kotlin mendukung berbagai bentuk fungsi — dari yang paling sederhana hingga yang sangat ekspresif.
// Fungsi dasar dengan parameter dan return type
fun tambah(a: Int, b: Int): Int {
return a + b
}
// Single-expression function — lebih ringkas untuk fungsi sederhana
fun tambah(a: Int, b: Int): Int = a + b
// Return type bisa diinfer jika single-expression
fun tambah(a: Int, b: Int) = a + b
Default Parameter dan Named Argument #
Kotlin mendukung default parameter — mengurangi kebutuhan function overloading yang berlebihan.
fun buatPengguna(
nama: String,
umur: Int = 0,
email: String = "",
aktif: Boolean = true
): String {
return "User($nama, umur=$umur, email=$email, aktif=$aktif)"
}
// Panggil dengan semua argumen
buatPengguna("Budi", 25, "[email protected]", true)
// Panggil dengan sebagian argumen (sisanya pakai default)
buatPengguna("Sari")
// Named argument — urutan bebas, lebih eksplisit
buatPengguna(nama = "Ahmad", aktif = false, email = "[email protected]")
Fungsi dengan Vararg #
fun jumlahkan(vararg angka: Int): Int {
return angka.sum()
}
println(jumlahkan(1, 2, 3)) // 6
println(jumlahkan(10, 20, 30, 40)) // 100
Kelas dan Objek #
Mendefinisikan Kelas #
class Mobil(
val merek: String,
val model: String,
var tahun: Int
) {
// Property tambahan
var odometer: Double = 0.0
// Method
fun jalan(jarakKm: Double) {
odometer += jarakKm
println("$merek $model melaju $jarakKm km. Total: $odometer km")
}
// toString otomatis yang deskriptif
override fun toString(): String {
return "$merek $model ($tahun) — ${odometer}km"
}
}
val mobil = Mobil("Toyota", "Avanza", 2022)
mobil.jalan(150.0)
mobil.jalan(75.5)
println(mobil) // Toyota Avanza (2022) — 225.5km
Data Class #
Untuk kelas yang fungsinya hanya menyimpan data, gunakan data class. Kotlin otomatis menghasilkan equals(), hashCode(), toString(), dan copy().
data class Mahasiswa(
val nama: String,
val nim: String,
val ipk: Double
)
val mhs1 = Mahasiswa("Budi", "2021001", 3.85)
val mhs2 = mhs1.copy(ipk = 3.90) // salin dengan nilai berbeda
println(mhs1) // Mahasiswa(nama=Budi, nim=2021001, ipk=3.85)
println(mhs1 == mhs2) // false (beda ipk)
Object — Singleton #
object digunakan untuk membuat singleton tanpa perlu pola desain manual.
object KonfigurasiApp {
val versi = "1.0.0"
val namaApp = "MyKotlinApp"
var debugMode = false
fun info() = "$namaApp v$versi (debug: $debugMode)"
}
println(KonfigurasiApp.info())
KonfigurasiApp.debugMode = true
Companion Object #
Companion object adalah padanan Kotlin untuk static member di Java.
class Koneksi private constructor(val url: String) {
companion object {
private var instance: Koneksi? = null
fun getInstance(url: String): Koneksi {
return instance ?: Koneksi(url).also { instance = it }
}
}
}
val db = Koneksi.getInstance("jdbc:postgresql://localhost/mydb")
Null Safety #
Null safety adalah salah satu fitur paling berharga di Kotlin. Di Java, NullPointerException adalah penyebab crash yang sangat umum. Kotlin memaksa kamu menangani kemungkinan null secara eksplisit di level tipe.
flowchart TD
A[Variabel Kotlin] --> B{Nullable?}
B -- Tidak --> C[Tipe Normal\nString, Int, dll]
B -- Ya --> D[Tipe Nullable\nString?, Int?, dll]
C --> E[Tidak bisa null\naman diakses langsung]
D --> F{Cara akses?}
F --> G["Safe call (?.)"]
F --> H["Elvis operator (?:)"]
F --> I["Non-null assertion (!!)"]
G --> J[Null jika objek null]
H --> K[Nilai default jika null]
I --> L["Exception jika null\n⚠ gunakan hati-hati"]// Tipe non-nullable: tidak bisa diisi null
var nama: String = "Budi"
// nama = null // ✗ error kompilasi!
// Tipe nullable: harus ditandai dengan ?
var namaNullable: String? = null
namaNullable = "Sari" // ✓ boleh
namaNullable = null // ✓ boleh
Safe Call Operator ?.
#
Mengakses property atau method hanya jika objeknya tidak null. Mengembalikan null jika objek null.
val nama: String? = ambilNamaDariDatabase()
// ANTI-PATTERN: cek null manual seperti Java
if (nama != null) {
println(nama.length)
}
// BENAR: safe call
println(nama?.length) // null jika nama null
// Chaining safe call
val panjangNamaDepan = pengguna?.profil?.namaDepan?.length
Elvis Operator ?:
#
Memberikan nilai default ketika ekspresi di kirinya null.
val nama: String? = ambilNama()
// Nilai default jika null
val tampilan = nama ?: "Pengguna Anonim"
// Bisa digabung dengan safe call
val panjang = nama?.length ?: 0
// Elvis dengan throw — validasi ringkas
val namaPasti = nama ?: throw IllegalArgumentException("Nama tidak boleh null")
Non-Null Assertion !!
#
Memberitahu compiler “percayakan padaku, ini tidak null”. Melempar NullPointerException jika ternyata null.
// ANTI-PATTERN: menggunakan !! sembarangan
val panjang = nama!!.length // crash jika nama null!
// BENAR: gunakan !! hanya jika kamu 100% yakin tidak null
// dan sudah ada pengecekan sebelumnya
val config = System.getenv("DATABASE_URL")
?: throw RuntimeException("DATABASE_URL harus diset")
// Setelah baris di atas, config pasti tidak null
val url = config // sudah aman, tidak perlu !!
Hindari!!sebisa mungkin. Setiap!!dalam kode adalah potensi crash yang belum ditangani. Jika kamu merasa perlu!!, pertimbangkan ulang alur logikamu — biasanya ada cara yang lebih aman dengan?.,?:, ataulet.
Let untuk Blok Eksekusi Kondisional #
val email: String? = ambilEmail()
// Eksekusi blok hanya jika email tidak null
email?.let { e ->
println("Mengirim email ke: $e")
kirimEmail(e)
}
Koleksi #
Kotlin membedakan koleksi menjadi dua kategori: immutable (tidak bisa diubah) dan mutable (bisa diubah). Ini eksplisit di level tipe.
// Immutable: tidak bisa tambah/hapus/ubah elemen
val daftarNama = listOf("Budi", "Sari", "Ahmad")
val himpunanAngka = setOf(1, 2, 3, 4, 5)
val kamus = mapOf("id" to "Indonesia", "en" to "English")
// Mutable: bisa dimodifikasi
val daftarDynamic = mutableListOf("Awal")
daftarDynamic.add("Tengah")
daftarDynamic.add("Akhir")
val mapDynamic = mutableMapOf<String, Int>()
mapDynamic["satu"] = 1
mapDynamic["dua"] = 2
Operasi Koleksi #
Kotlin menyediakan operasi functional yang kaya untuk memproses koleksi tanpa loop manual.
val mahasiswa = listOf(
mapOf("nama" to "Budi", "ipk" to 3.85, "jurusan" to "Informatika"),
mapOf("nama" to "Sari", "ipk" to 3.92, "jurusan" to "Matematika"),
mapOf("nama" to "Ahmad", "ipk" to 3.71, "jurusan" to "Informatika"),
mapOf("nama" to "Rina", "ipk" to 3.60, "jurusan" to "Fisika"),
)
// filter — ambil yang memenuhi kondisi
val ipkTinggi = mahasiswa.filter { it["ipk"] as Double >= 3.80 }
// map — transformasi setiap elemen
val namaSaja = mahasiswa.map { it["nama"] }
// sortedBy — urutkan
val terurut = mahasiswa.sortedByDescending { it["ipk"] as Double }
// groupBy — kelompokkan
val perJurusan = mahasiswa.groupBy { it["jurusan"] }
// find — cari satu elemen
val mahasiswaBudi = mahasiswa.find { it["nama"] == "Budi" }
// any dan all — cek kondisi
val adaYangIpkPerfect = mahasiswa.any { (it["ipk"] as Double) == 4.0 }
val semuaLulus = mahasiswa.all { (it["ipk"] as Double) >= 3.0 }
Lambda dan Higher-Order Function #
Lambda adalah fungsi anonim yang bisa disimpan dalam variabel dan diteruskan sebagai argumen. Higher-order function adalah fungsi yang menerima atau mengembalikan fungsi lain.
// Lambda dasar
val sapa: (String) -> String = { nama -> "Halo, $nama!" }
println(sapa("Budi")) // Halo, Budi!
// Lambda dengan implicit parameter 'it' (untuk satu parameter)
val kuadrat: (Int) -> Int = { it * it }
println(kuadrat(5)) // 25
// Higher-order function
fun prosesAngka(angka: Int, operasi: (Int) -> Int): Int {
return operasi(angka)
}
println(prosesAngka(10) { it * 2 }) // 20 — lambda trailing
println(prosesAngka(10) { it + 100 }) // 110
Trailing Lambda #
Jika parameter terakhir sebuah fungsi adalah lambda, kamu bisa menulisnya di luar tanda kurung — ini disebut trailing lambda syntax dan membuat kode jauh lebih bersih.
// ANTI-PATTERN: lambda di dalam kurung
daftarNama.forEach({ nama -> println(nama) })
// BENAR: trailing lambda
daftarNama.forEach { nama -> println(nama) }
// Dengan implicit 'it'
daftarNama.forEach { println(it) }
Extension Functions #
Extension function memungkinkan kamu menambahkan fungsi baru ke kelas yang sudah ada — bahkan kelas dari library pihak ketiga atau standard library — tanpa inheritance.
// Tambahkan fungsi ke String
fun String.palindrom(): Boolean {
val bersih = this.lowercase().replace(" ", "")
return bersih == bersih.reversed()
}
println("kasur rusak".palindrom()) // true
println("kotlin".palindrom()) // false
// Tambahkan fungsi ke Int
fun Int.kaliPersen(persen: Int): Double {
return this * persen / 100.0
}
println(500_000.kaliPersen(10)) // 50000.0
// Tambahkan fungsi ke List
fun <T> List<T>.cetakSemua() {
forEachIndexed { i, item -> println("$i. $item") }
}
listOf("Satu", "Dua", "Tiga").cetakSemua()
Extension function sangat berguna untuk menulis kode yang terasa natural. Alih-alih StringUtils.capitalize(str), kamu bisa menulis str.capitalize().
Extension function tidak benar-benar mengubah kelas yang diperluas. Mereka dikompilasi menjadi static method biasa di JVM. Ini berarti extension function tidak bisa mengakses private member dari kelas yang diperluas.
Ringkasan #
valvsvar— gunakanvalsebagai default, beralih kevarhanya jika nilainya memang perlu berubah. Kode denganvallebih mudah diprediksi dan aman.- Type inference — tidak perlu selalu menulis tipe secara eksplisit; biarkan compiler menebak dari nilai yang diberikan. Tulis tipe eksplisit hanya jika tipenya tidak jelas atau berbeda dari yang diinfer.
ifdanwhenadalah ekspresi — keduanya mengembalikan nilai dan bisa digunakan di sisi kanan assignment, menggantikan ternary operator dan switch yang verbose.- String template — gunakan
$variabelatau${ekspresi}alih-alih concatenation manual dengan+.- Null safety di level tipe — tipe non-nullable tidak bisa diisi null; tipe nullable ditandai dengan
?. Gunakan?.,?:, danletuntuk menangani nullable dengan aman. Hindari!!kecuali benar-benar terpaksa.data classuntuk objek data — otomatis mendapatequals,hashCode,toString, dancopy. Gunakan ini untuk model, DTO, dan objek sejenis.- Koleksi immutable sebagai default — gunakan
listOf,setOf,mapOfsebagai default; beralih kemutableListOfdll hanya jika memang perlu modifikasi.- Extension function — cara elegan untuk menambahkan fungsionalitas ke kelas yang sudah ada tanpa inheritance, menghasilkan kode yang terasa lebih natural dan mudah dibaca.