Banyak pemula terkejut saat memanggil nama.uppercase() dan menemukan variabel nama tetap bernilai kecil. Perilaku ini bukan galat, melainkan konsekuensi langsung dari sifat immutable pada String Kotlin. Pada chapter ini kita akan membedah mengapa String tidak bisa diubah, bagaimana method string selalu mengembalikan instance baru, dan apa yang sebenarnya terjadi saat kita “mengubah” variabel string. Pemahaman ini krusial agar kita tidak menulis kode yang tampak berfungsi tetapi diam-diam kehilangan hasil transformasi.
Konsep Immutability #
Sebuah objek disebut immutable bila keadaannya tidak dapat diubah setelah dibuat. Tipe String di Kotlin (seperti java.lang.String di JVM) bersifat immutable: sekali instance string terbentuk, urutan karakter di dalamnya tidak bisa dimodifikasi. Semua method yang terlihat “mengubah” string sebenarnya membuat instance baru yang berisi hasil transformasi.
val nama = "Budi"
val besar = nama.uppercase() // instance baru "BUDI"
// nama masih "Budi"Tipe String tidak memiliki field publik, tidak memiliki method setCharAt(i, c), dan tidak menyediakan operator indeks untuk penugasan seperti s[0] = 'X'. Kompiler akan menolak kode tersebut.
Percobaan Mutasi yang Gagal #
Mari kita lihat percobaan umum yang sering dijalankan pemula dan hasilnya:
val nama = "Budi"
nama.uppercase() // hasil dibuang, nama tetap "Budi"
println(nama) // tetap "Budi"Pemanggilan nama.uppercase() mengembalikan string baru "BUDI", tetapi nilai kembalian tidak ditangkap ke variabel apa pun. Variabel nama tetap menunjuk ke instance asli "Budi". Untuk mendapatkan efek yang diinginkan, hasilnya harus ditangkap:
val nama = "Budi"
val namaBesar = nama.uppercase()
println(namaBesar) // "BUDI"Hal serupa berlaku untuk replace, trim, substring, lowercase, dan seluruh method transformasi string lainnya. Tidak ada satu pun yang memodifikasi string penerima.
Reassign Variabel Bukan Mengubah Instance #
Menggunakan var (variabel yang bisa ditugasi ulang) sering dianggap sebagai cara mengubah string, padahal kita hanya mengganti instance yang dirujuk:
var bisaBerubah = "halo"
bisaBerubah = bisaBerubah.uppercase()
println(bisaBerubah) // "HALO"Pada baris kedua, ekspresi bisaBerubah.uppercase() membuat instance baru "HALO", lalu referensi bisaBerubah dipindahkan dari "halo" ke "HALO". Instance "halo" yang lama masih ada di memori sampai garbage collector membuangnya. Inilah perbedaan antara mengubah instance dan mengubah referensi variabel.
Identitas Referensi: === vs == #
Operator == pada String membandingkan isi (memanggil equals), sedangkan === membandingkan identitas referensi apakah kedua variabel menunjuk ke instance yang sama. Karena method seperti uppercase() selalu membuat instance baru, hasil dua pemanggilan uppercase() pada string yang sama akan memiliki isi yang identik tetapi referensi yang berbeda:
val asli = "abc"
val hasil1 = asli.uppercase() // instance "ABC" #1
val hasil2 = asli.uppercase() // instance "ABC" #2
println(hasil1 == hasil2) // true, isi sama
println(hasil1 === hasil2) // false, beda instanceUntuk pemrograman sehari-hari, gunakan == karena yang penting adalah isi teksnya. Pengecekan === hanya relevan untuk optimasi memori tingkat lanjut atau ketika kita ingin menguji apakah string interning menggabungkan dua literal.
Contoh Lengkap dan Outputnya #
File imutabilitas.kt berikut menunjukkan semua perilaku di atas secara terukur:
fun main() {
val nama = "Budi"
println("nama awal: $nama")
val namaBesar = nama.uppercase()
println("nama.uppercase(): $namaBesar")
println("nama setelah uppercase() dipanggil: $nama")
val pesan = "halo dunia"
println("pesan awal: $pesan")
val pesanDiperbarui = pesan.replace("halo", "selamat pagi")
println("pesan.replace(...): $pesanDiperbarui")
println("pesan asli setelah replace: $pesan")
val teks = "kotlin"
val diambil = teks.substring(0, 3)
println("teks.substring(0,3): $diambil")
println("teks asli: $teks")
val asli = "abc"
println("Identitas referensi asli === asli: ${asli === asli}")
val hasil1 = asli.uppercase()
val hasil2 = asli.uppercase()
println("hasil1 === hasil2? ${hasil1 === hasil2}")
println("hasil1 == hasil2? ${hasil1 == hasil2}")
var bisaBerubah = "halo"
println("bisaBerubah awal: $bisaBerubah")
bisaBerubah = bisaBerubah.uppercase()
println("bisaBerubah setelah reassign: $bisaBerubah")
}Output yang Dihasilkan:
nama awal: Budi
nama.uppercase(): BUDI
nama setelah uppercase() dipanggil: Budi
pesan awal: halo dunia
pesan.replace(...): selamat pagi dunia
pesan asli setelah replace: halo dunia
teks.substring(0,3): kot
teks asli: kotlin
Identitas referensi asli === asli: true
hasil1 === hasil2? false
hasil1 == hasil2? true
bisaBerubah awal: halo
bisaBerubah setelah reassign: HALOMari kita telaah bagian penting dari kode di atas. Baris val namaBesar = nama.uppercase() mendemonstrasikan bahwa nama tetap bernilai "Budi" setelah pemanggilan method. Baris pesan.replace("halo", "selamat pagi") menghasilkan string baru "selamat pagi dunia", sementara pesan asli masih "halo dunia". Operator === menunjukkan dua instance uppercase() yang berbeda walaupun isinya sama. Terakhir, baris bisaBerubah = bisaBerubah.uppercase() bukan mengubah instance, melainkan mengganti referensi variabel ke instance baru.
Dampak terhadap Performa #
Sifat immutable memiliki konsekuensi penting terhadap performa saat kita melakukan banyak konkatenasi. Setiap s += "x" menciptakan instance string baru dengan panjang s.length + 1, lalu menyalin seluruh karakter lama ke instance baru. Jika operasi ini diulang 10.000 kali, total karakter yang disalin bisa mencapai puluhan juta. Untuk skenario semacam itu, Kotlin menyediakan StringBuilder yang bersifat mutable di tingkat internal — sebuah topik yang akan kita bahas tuntas pada chapter StringBuilder.
Referensi Lanjutan #
- Strings
— penjelasan resmi sifat immutable
Stringdi Kotlin
Penutup #
Dengan pemahaman bahwa setiap method string mengembalikan instance baru, kita siap memanfaatkan string template — fitur Kotlin yang membuat penyusunan teks dinamis jauh lebih ringkas daripada konkatenasi manual. Materi tersebut menjadi fokus chapter String Template.