Variabel

Variabel #

Variabel adalah tempat menyimpan data selama program berjalan. Di bahasa lain seperti Java atau JavaScript, semua variabel pada dasarnya bisa diubah nilainya — kamu yang harus disiplin menjaga mana yang boleh berubah dan mana yang tidak. Kotlin mengubah pendekatan ini: ia memaksamu memilih sejak awal apakah sebuah variabel boleh berubah atau tidak, melalui dua kata kunci berbeda: val dan var. Pilihan ini bukan sekadar gaya — ia punya dampak nyata pada keamanan dan keterbacaan kode. Artikel ini membahas val dan var secara mendalam, beserta fitur-fitur terkait variabel seperti type inference, scope, null safety, lateinit, by lazy, dan destructuring.

val — Variabel yang Tidak Bisa Di-reassign #

val adalah singkatan dari value. Setelah diinisialisasi, referensinya tidak bisa diganti ke nilai lain. Ini yang paling sering disebut “immutable variable” meskipun istilah yang lebih tepat adalah read-only reference — karena objek yang dirujuk val bisa saja bisa dimodifikasi secara internal.

val nama = "Budi"
val tahunLahir = 1995
val aktif = true

// nama = "Sari"  // ✗ error: Val cannot be reassigned

Perbedaan antara read-only reference dan immutable object ini penting dipahami:

// val hanya mengunci REFERENSI, bukan isi objeknya
val daftar = mutableListOf("apel", "mangga")

// daftar = mutableListOf("jeruk")  // ✗ tidak bisa — referensi dikunci

daftar.add("jeruk")    // ✓ bisa — isi objek dimodifikasi
daftar.remove("apel")  // ✓ bisa
println(daftar)        // [mangga, jeruk]

Jika kamu ingin objeknya benar-benar tidak bisa dimodifikasi, gunakan koleksi immutable:

// BENAR: referensi dan isi sama-sama terlindungi
val daftarTetap = listOf("apel", "mangga", "jeruk")
// daftarTetap.add("anggur")  // ✗ error kompilasi — List tidak punya method add()

val Tidak Harus Diinisialisasi Langsung #

val bisa dideklarasikan tanpa nilai awal, selama ia pasti diinisialisasi sebelum digunakan — dan hanya diinisialisasi sekali. Compiler Kotlin cukup cerdas untuk melacak ini.

val pesan: String

val kondisi = true

if (kondisi) {
    pesan = "Kondisi terpenuhi"
} else {
    pesan = "Kondisi tidak terpenuhi"
}

println(pesan)  // aman — compiler yakin pesan sudah diinisialisasi
// ANTI-PATTERN: val yang diinisialisasi lebih dari sekali
val skor: Int
skor = 80
// skor = 90  // ✗ error — sudah diinisialisasi sebelumnya

var — Variabel yang Bisa Di-reassign #

var adalah singkatan dari variable. Nilainya bisa diganti kapan saja selama program berjalan.

var skor = 0
println(skor)   // 0

skor = 100
println(skor)   // 100

skor += 50
println(skor)   // 150

skor--
println(skor)   // 149

var cocok untuk data yang memang berubah seiring waktu: skor game, status koneksi, counter, nilai akumulasi dalam loop, dan semacamnya.

var statusKoneksi = "Disconnected"
var percobaan = 0

while (percobaan < 3) {
    val berhasil = cobaSambungkan()
    if (berhasil) {
        statusKoneksi = "Connected"
        break
    }
    percobaan++
    statusKoneksi = "Retrying... ($percobaan/3)"
}

println(statusKoneksi)

Memilih antara val dan var #

Ini adalah keputusan yang kamu buat puluhan kali sehari. Panduan sederhananya:

GUNAKAN val jika:
  ✓ Nilai tidak perlu berubah setelah diset
  ✓ Hasilnya dihitung sekali dan dipakai berkali-kali
  ✓ Kamu ragu — coba val dulu, ganti ke var kalau perlu

GUNAKAN var jika:
  ✓ Nilai memang akan berubah selama eksekusi (counter, akumulator)
  ✓ Variabel diinisialisasi dalam kondisi tertentu (if/else/try)
  ✓ State yang harus diperbarui dari luar (misal: properti kelas yang bisa diset)
// ANTI-PATTERN: pakai var padahal nilainya tidak pernah berubah
var urlBase = "https://api.example.com"
var versiApi = "v2"
var timeout = 30_000

// BENAR: gunakan val karena nilainya konstan di runtime
val urlBase = "https://api.example.com"
val versiApi = "v2"
val timeout = 30_000

Menggunakan val sebagai default bukan sekadar gaya — ini membuat kode lebih mudah di-reason. Saat kamu membaca kode dan menemukan val, kamu tahu referensinya tidak akan berubah di bawah. Saat melihat var, kamu tahu perlu melacak ke mana nilainya bisa berubah.


Type Inference — Tipe Otomatis dari Nilai #

Kotlin bisa menebak tipe variabel dari nilai yang diberikan. Kamu tidak perlu selalu menuliskan tipenya secara eksplisit.

val nama = "Rina"          // → String
val umur = 27              // → Int
val tinggi = 165.5         // → Double
val aktif = true           // → Boolean
val karakter = 'K'         // → Char
val populasi = 8_000_000L  // → Long (suffix L)
val suhu = 36.5f           // → Float (suffix f)

Kapan Menulis Tipe Secara Eksplisit #

Type inference tidak berarti tipe tidak penting — ia hanya mengurangi redundansi. Ada situasi di mana menulis tipe eksplisit lebih baik:

// Perlu eksplisit: compiler menginfer Double, tapi kamu mau Float
val rasio: Float = 0.75

// Perlu eksplisit: variabel dideklarasi tanpa nilai awal
var hasil: Int
hasil = hitungSesuatu()

// Perlu eksplisit: tipe lebih general dari nilai konkret
val daftar: List<String> = mutableListOf("a", "b")
// → tipe terlihat sebagai List, bukan MutableList — interface lebih sempit yang diekspos

// Perlu eksplisit: kejelasan untuk pembaca
val koneksiDb: DatabaseConnection = KonfigurasiDb.buat()
// lebih jelas daripada membiarkan pembaca menebak tipe kembalian KonfigurasiDb.buat()
// ANTI-PATTERN: eksplisit tipe yang sudah jelas dari nilai
val nama: String = "Budi"
val umur: Int = 25
val aktif: Boolean = true

// BENAR: biarkan compiler menebak
val nama = "Budi"
val umur = 25
val aktif = true

Scope Variabel #

Scope (cakupan) menentukan di mana sebuah variabel bisa diakses. Di Kotlin, variabel hidup dalam blok { } tempat ia dideklarasikan.

fun contohScope() {
    val luarBlok = "bisa diakses di mana saja dalam fungsi ini"
    
    if (true) {
        val dalamIf = "hanya ada di dalam blok if ini"
        println(luarBlok)   // ✓ bisa
        println(dalamIf)    // ✓ bisa
    }
    
    // println(dalamIf)     // ✗ error — dalamIf sudah di luar scope
    println(luarBlok)       // ✓ masih bisa
}

Shadowing #

Kotlin mengizinkan variabel di scope dalam untuk menyembunyikan (shadow) variabel dengan nama yang sama di scope luar:

val pesan = "pesan luar"

run {
    val pesan = "pesan dalam"  // ✓ valid — shadow variabel luar
    println(pesan)  // pesan dalam
}

println(pesan)  // pesan luar — tidak terpengaruh
Shadowing membuat kode sulit dibaca dan rawan bug — variabel mana yang sedang dimodifikasi? Hindari memberikan nama yang sama untuk variabel di scope berbeda dalam satu fungsi yang sama, kecuali dalam kasus seperti lambda parameter yang memang konvensional singkat seperti it.

Variabel Top-Level #

Kotlin mengizinkan variabel dideklarasikan di luar kelas dan fungsi — langsung di level file (top-level). Variabel ini bisa diakses dari mana saja dalam package yang sama.

// File: Konstanta.kt
package com.myapp.config

var hitungAkses = 0  // top-level var — bisa diakses dari seluruh package

val NAMA_APLIKASI = "MyApp"  // lebih baik pakai const val untuk konstanta — lihat artikel Konstanta

Variabel Nullable #

Secara default, semua variabel Kotlin adalah non-nullable — tidak bisa diisi null. Untuk mengizinkan null, tambahkan ? pada tipe.

// Non-nullable: tidak bisa null
var nama: String = "Budi"
// nama = null  // ✗ error kompilasi

// Nullable: bisa null
var alamat: String? = null
alamat = "Jl. Merdeka No. 1"
alamat = null  // ✓ boleh

Mengakses Variabel Nullable dengan Aman #

var email: String? = ambilEmailPengguna()

// ANTI-PATTERN: akses langsung tanpa pengecekan
val panjang = email.length  // ✗ error kompilasi — email bisa null

// BENAR: safe call
val panjang = email?.length  // Int? — bisa null jika email null

// BENAR: dengan nilai default via Elvis
val panjang = email?.length ?: 0  // Int — 0 jika email null

// BENAR: cek eksplisit
if (email != null) {
    println(email.length)  // dalam blok ini, compiler tahu email tidak null
}

// BENAR: let untuk eksekusi blok hanya jika tidak null
email?.let { e ->
    println("Kirim ke: $e")
    kirimEmail(e)
}

Smart Cast #

Setelah kamu mengecek null secara eksplisit, Kotlin otomatis men-cast variabel ke tipe non-nullable dalam blok tersebut — tanpa perlu cast manual.

var teks: String? = ambilTeks()

if (teks != null) {
    // Di sini teks otomatis dianggap String (non-nullable)
    println(teks.uppercase())   // ✓ tidak perlu teks?.uppercase()
    println(teks.length)        // ✓ aman
}

// Smart cast juga bekerja dengan when
when {
    teks == null -> println("Teks kosong")
    teks.isBlank() -> println("Teks hanya spasi")
    else -> println("Teks: $teks")  // di sini teks sudah pasti non-null dan non-blank
}

lateinit — Inisialisasi Ditunda #

lateinit digunakan untuk properti var yang dijamin akan diinisialisasi sebelum digunakan, tapi tidak bisa diinisialisasi di saat deklarasi. Ini paling sering dipakai di framework seperti Spring atau Android di mana dependency injection atau lifecycle framework yang menginisialisasi nilai.

class PenggunaViewModel {
    lateinit var repository: PenggunaRepository
    lateinit var nama: String
    
    fun inisialisasi(repo: PenggunaRepository) {
        repository = repo
        nama = "Default"
    }
    
    fun tampilkan() {
        println("Pengguna: $nama")
        repository.ambilSemua()
    }
}

Aturan lateinit #

lateinit hanya bisa digunakan dengan kondisi tertentu:

lateinit HANYA BISA untuk:
  ✓ var (bukan val)
  ✓ Tipe non-nullable
  ✓ Tipe referensi (String, kelas kustom, dll)

lateinit TIDAK BISA untuk:
  ✗ val
  ✗ Tipe nullable (String?, Int?)
  ✗ Tipe primitif (Int, Double, Boolean — gunakan wrapper atau inisialisasi langsung)

Memeriksa Apakah lateinit Sudah Diinisialisasi #

Mengakses lateinit sebelum diinisialisasi melempar UninitializedPropertyAccessException. Gunakan ::namaProperti.isInitialized untuk mengecek sebelum akses:

class Layanan {
    lateinit var klien: HttpClient
    
    fun kirimPermintaan(url: String): String {
        if (!::klien.isInitialized) {
            throw IllegalStateException("Klien HTTP belum diinisialisasi. Panggil setup() dulu.")
        }
        return klien.get(url)
    }
    
    fun setup(klien: HttpClient) {
        this.klien = klien
    }
}

by lazy — Inisialisasi Saat Pertama Kali Diakses #

by lazy memungkinkan inisialisasi ditunda sampai variabel pertama kali diakses. Blok lambda yang diberikan hanya dieksekusi sekali — hasilnya disimpan dan digunakan untuk akses berikutnya.

val koneksiDb: DatabaseConnection by lazy {
    println("Membuka koneksi database...")  // hanya dicetak sekali
    DatabaseConnection.buat("jdbc:postgresql://localhost/mydb")
}

fun main() {
    println("Aplikasi dimulai")
    
    // koneksiDb belum diinisialisasi di sini
    
    val hasil = koneksiDb.query("SELECT * FROM users")  // inisialisasi terjadi di sini
    println(hasil)
    
    val hasil2 = koneksiDb.query("SELECT * FROM orders")  // tidak inisialisasi ulang
    println(hasil2)
}

Output:

Aplikasi dimulai
Membuka koneksi database...
[hasil query pertama]
[hasil query kedua]

Kapan by lazy Lebih Tepat dari Inisialisasi Langsung #

GUNAKAN by lazy jika:
  ✓ Inisialisasi mahal (koneksi DB, load file besar, parsing kompleks)
  ✓ Nilainya mungkin tidak pernah digunakan — tunda sampai pasti dibutuhkan
  ✓ Nilai butuh konteks yang belum tersedia saat objek dibuat
  ✓ Kamu ingin jaminan thread-safety untuk inisialisasi (default mode lazy)

JANGAN pakai by lazy jika:
  ✗ Inisialisasinya ringan — overhead lazy tidak worth it
  ✗ Nilainya pasti selalu dipakai — langsung inisialisasi saja
  ✗ Kamu butuh var — lazy hanya untuk val

Mode Thread-Safety by lazy #

by lazy secara default menggunakan LazyThreadSafetyMode.SYNCHRONIZED — hanya satu thread yang bisa menginisialisasi, thread lain menunggu. Ada tiga mode yang tersedia:

// SYNCHRONIZED (default): aman di multi-thread, sedikit overhead
val data by lazy { muatData() }

// PUBLICATION: beberapa thread bisa menginisialisasi, tapi hanya satu yang dipakai
val data by lazy(LazyThreadSafetyMode.PUBLICATION) { muatData() }

// NONE: tidak ada sinkronisasi — gunakan hanya jika pasti single-thread
val data by lazy(LazyThreadSafetyMode.NONE) { muatData() }

Destructuring Declaration #

Kotlin mengizinkan kamu mengekstrak beberapa nilai dari sebuah objek sekaligus ke variabel terpisah dalam satu baris. Ini disebut destructuring declaration.

// Dari Pair
val koordinat = Pair(10.5, 106.8)
val (lintang, bujur) = koordinat
println("Lintang: $lintang, Bujur: $bujur")

// Dari data class
data class Titik(val x: Int, val y: Int, val label: String)

val titik = Titik(3, 7, "A")
val (x, y, label) = titik
println("Titik $label ada di ($x, $y)")

// Dalam loop — sangat umum digunakan dengan Map
val kamus = mapOf("id" to "Indonesia", "en" to "English", "ja" to "Jepang")

for ((kode, bahasa) in kamus) {
    println("$kode$bahasa")
}

Jika ada komponen yang tidak kamu butuhkan, gunakan _ sebagai placeholder:

val (_, bujur) = koordinat       // abaikan lintang
val (x, _, label) = titik        // abaikan y

Konvensi Penamaan Variabel #

Kotlin mengikuti konvensi camelCase untuk variabel dan properti:

// BENAR: camelCase
val namaLengkap = "Budi Santoso"
var jumlahProduk = 0
val urlGambarProfil = "https://cdn.example.com/foto.jpg"

// ANTI-PATTERN: snake_case (konvensi Python/SQL, bukan Kotlin)
val nama_lengkap = "Budi Santoso"
var jumlah_produk = 0

// ANTI-PATTERN: PascalCase (konvensi untuk kelas, bukan variabel)
val NamaLengkap = "Budi Santoso"

Nama variabel harus deskriptif tapi tidak bertele-tele. Hindari singkatan yang tidak jelas:

// ANTI-PATTERN: terlalu singkat dan tidak jelas
val n = "Budi"
val t = 30_000
val f = ambilDaftar()

// BENAR: deskriptif
val namaPengguna = "Budi"
val timeoutMs = 30_000
val daftarProduk = ambilDaftar()

Untuk variabel Boolean, gunakan awalan yang mencerminkan nilai true/false:

// BENAR: awalan yang jelas untuk Boolean
val isAktif = true
val sudahVerifikasi = false
val bolehEdit = true
val adaKoneksi = false

Perbandingan lateinit vs by lazy #

Aspeklateinitby lazy
Keywordvarval
Waktu inisialisasiKapan saja sebelum diaksesSaat pertama kali diakses
Yang menginisialisasiKode kamu secara eksplisitLambda yang kamu definisikan
Thread-safetyTidak ada (kamu yang urus)Ada (SYNCHRONIZED by default)
Bisa dicek::prop.isInitializedTidak perlu (selalu ada setelah akses pertama)
Tipe yang didukungTipe referensi non-nullableSemua tipe
Use case umumDependency injection, Android lifecycleKomputasi mahal, koneksi resource

Ringkasan #

  • val adalah default — gunakan val untuk semua variabel yang nilainya tidak perlu berubah setelah diset. Ganti ke var hanya jika ada alasan konkret untuk itu.
  • val mengunci referensi, bukan isi objekval daftar = mutableListOf(...) masih memungkinkan isi list dimodifikasi; yang terkunci hanyalah referensi daftar itu sendiri.
  • Type inference mengurangi redundansi — tidak perlu menulis tipe jika sudah jelas dari nilai. Tulis eksplisit hanya untuk kejelasan atau ketika tipe yang diinginkan berbeda dari yang akan diinfer.
  • Scope variabel dibatasi oleh blok — variabel hanya bisa diakses di dalam blok { } tempat ia dideklarasikan. Hindari shadowing (nama sama di scope berbeda) untuk menjaga keterbacaan.
  • Null safety dimulai dari deklarasi — tipe tanpa ? tidak bisa null. Tambahkan ? hanya jika nilai memang bisa null, dan tangani dengan ?., ?:, atau let.
  • Smart cast memudahkan penanganan nullable — setelah cek null eksplisit, Kotlin otomatis memperlakukan variabel sebagai non-nullable dalam blok tersebut.
  • lateinit untuk injeksi dan lifecycle — gunakan ketika framework yang menginisialisasi nilai, bukan kodeму sendiri saat konstruksi. Selalu gunakan isInitialized sebelum akses jika ada keraguan.
  • by lazy untuk inisialisasi mahal — nilai dihitung hanya saat pertama dibutuhkan, disimpan, dan thread-safe secara default. Ideal untuk koneksi database, load konfigurasi, atau komputasi berat.
  • Destructuring — ekstrak beberapa nilai dari data class, Pair, atau Map entry ke variabel terpisah dalam satu baris. Gunakan _ untuk komponen yang tidak diperlukan.

← Sebelumnya: Komentar   Berikutnya: Konstanta →

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