Karena String bersifat immutable, setiap konkatenasi dengan operator + menciptakan instance baru dan menyalin seluruh karakter lama. Operasi ini murah untuk satu atau dua kali penggabungan, tetapi menjadi sangat mahal ketika diulang ribuan kali dalam loop. Kotlin menyediakan StringBuilder sebagai solusi: sebuah tipe mutable yang menyimpan karakter pada buffer internal sehingga append, insert, dan delete tidak perlu menyalin ulang seluruh konten. Pada chapter ini kita akan mempelajari API StringBuilder, mengukur perbedaan performa dengan operator +, dan mengenal helper buildString yang membuat penggunaan StringBuilder lebih ringkas.

Mengapa Butuh StringBuilder #

Setiap kali kita menulis s += "x", Kotlin melakukan tiga hal: mengalokasikan instance string baru dengan panjang s.length + 1, menyalin karakter s ke instance baru, dan menyalin karakter "x" ke posisi terakhir. Jika loop berjalan 10.000 kali, total karakter yang disalin bisa mencapai sekitar 50 juta. StringBuilder menyimpan karakter pada array internal yang dapat diperbesar, sehingga append cukup menulis karakter pada posisi kosong berikutnya.

kotlin Contoh: pola tidak efisien
var s = ""
for (i in 1..1000) {
    s += "x"   // 1000 instance String baru
}

Pola di atas bekerja, tetapi membuang memori dan CPU. Pola yang setara namun efisien menggunakan StringBuilder:

kotlin Contoh: pola efisien
val sb = StringBuilder()
for (i in 1..1000) {
    sb.append("x")   // menulis ke buffer internal
}
val hasil = sb.toString()

API Dasar: append, insert, delete #

Method append(value) menambahkan representasi string dari value ke akhir buffer. Method insert(index, value) menyisipkan pada posisi tertentu. Method delete(start, end) menghapus karakter dari start hingga end - 1. Semua method ini mengembalikan instance StringBuilder yang sama, sehingga bisa dirantaikan:

kotlin Contoh: operasi dasar StringBuilder
val sb = StringBuilder()
sb.append("Halo").append(", ").append("Kotlin").append("!")
println(sb)                  // Halo, Kotlin!
sb.insert(4, " Dunia")       // sisipkan di indeks 4
println(sb)                  // Halo Dunia, Kotlin!
sb.delete(4, 10)             // hapus " Dunia"
println(sb)                  // Halo, Kotlin!

Method setLength(n) memotong buffer ke panjang n (atau menambah karakter null bila n > length). Method reverse() membalik urutan karakter dalam buffer. Semua manipulasi terjadi pada instance yang sama tanpa membuat instance baru.

toString dan Properti capacity #

Method toString() mengembalikan String berisi salinan karakter pada buffer. Karena String immutable, hasil ini menjadi snapshot pada saat pemanggilan. Properti length mengembalikan jumlah karakter aktif, sedangkan capacity() mengembalikan ukuran array internal (yang bisa lebih besar dari length). Saat length melebihi capacity, StringBuilder otomatis memperbesar buffer (biasanya menjadi dua kali lipat ditambah dua).

kotlin Contoh: capacity otomatis
val sb = StringBuilder()
println(sb.capacity())      // 16 (default)
sb.append("x".repeat(20))
println(sb.capacity())      // 34 (diperbesar otomatis)
println(sb.length)          // 20

Untuk menghindari perbesaran buffer yang berulang, kita bisa memilih konstruktor StringBuilder(capacity) yang mengalokasikan ukuran awal sesuai kebutuhan.

buildString — Helper yang Ringkas #

Kotlin menyediakan function buildString(builderAction) yang membuat instance StringBuilder baru dan menjalankan lambda receiver dengan this merujuk ke StringBuilder tersebut. Hasil akhir dikembalikan sebagai String. Pola ini menghilangkan kebutuhan deklarasi variabel eksplisit dan toString() manual:

kotlin Contoh: buildString
val teks = buildString {
    append("Baris 1\n")
    append("Baris 2\n")
    append("Baris 3")
}
println(teks)

Function ini juga memungkinkan inisialisasi kapasitas melalui overload buildString(capacity, builderAction).

Contoh Lengkap dan Outputnya #

File string_builder.kt berikut menunjukkan seluruh API dan perbandingan performa:

kotlin
fun main() {
    val sb = StringBuilder()
    sb.append("Halo")
    sb.append(", ")
    sb.append("Kotlin")
    sb.append("!")
    println("Setelah append: $sb")

    sb.insert(4, " Dunia")
    println("Setelah insert di indeks 4: $sb")

    sb.delete(4, 10)
    println("Setelah delete: $sb")

    sb.setLength(0)
    println("Kapasitas awal: ${sb.capacity()}")
    sb.append("a".repeat(20))
    println("Kapasitas setelah append 20 char: ${sb.capacity()}")

    val hasil = sb.toString()
    println("toString(): $hasil")
    println("Panjang hasil: ${hasil.length}")

    val ukuran = 10_000
    val mulaiConcat = System.nanoTime()
    var s = ""
    for (i in 0 until ukuran) {
        s += "x"
    }
    val akhirConcat = System.nanoTime()
    println("Concat (+) ${ukuran}x — panjang: ${s.length}, durasi ns: ${akhirConcat - mulaiConcat}")

    val mulaiSB = System.nanoTime()
    val builder = StringBuilder()
    for (i in 0 until ukuran) {
        builder.append("x")
    }
    val hasilBuilder = builder.toString()
    val akhirSB = System.nanoTime()
    println("StringBuilder ${ukuran}x — panjang: ${hasilBuilder.length}, durasi ns: ${akhirSB - mulaiSB}")

    println("StringBuilder lebih cepat? ${akhirSB - mulaiSB < akhirConcat - mulaiConcat}")

    val teks = buildString {
        append("Baris 1\n")
        append("Baris 2\n")
        append("Baris 3")
    }
    println("Hasil buildString:")
    println(teks)
}

Output yang Dihasilkan:

text
Setelah append: Halo, Kotlin!
Setelah insert di indeks 4: Halo Dunia, Kotlin!
Setelah delete: Halo, Kotlin!
Kapasitas awal: 34
Kapasitas setelah append 20 char: 34
toString(): aaaaaaaaaaaaaaaaaaaa
Panjang hasil: 20
Concat (+) 10000x — panjang: 10000, durasi ns: 39918720
StringBuilder 10000x — panjang: 10000, durasi ns: 1108712
StringBuilder lebih cepat? true
Hasil buildString:
Baris 1
Baris 2
Baris 3

Mari kita telaah bagian penting dari kode di atas. Baris sb.append("Halo") menulis karakter ke buffer internal tanpa membuat instance String baru. Operasi sb.insert(4, " Dunia") menyisipkan string pada posisi indeks 4 dengan menggeser karakter setelahnya. Baris sb.delete(4, 10) menghapus enam karakter mulai indeks 4. Bagian perbandingan performa menggunakan System.nanoTime() untuk mengukur durasi; angka aktual bisa berbeda antar mesin, tetapi pola umumnya adalah StringBuilder jauh lebih cepat. Terakhir, buildString { ... } adalah singkatan idiomatik untuk StringBuilder().apply { ... }.toString().

Kapan Tidak Perlu StringBuilder #

Tidak semua konkatenasi membutuhkan StringBuilder. Untuk menyusun string dari beberapa variabel dalam satu ekspresi, string template "$a $b $c" sudah cukup dan lebih mudah dibaca. StringBuilder juga tidak diperlukan jika kita hanya menggabungkan dua atau tiga string di luar loop — kompilator Kotlin akan mengoptimalkan ekspresi a + b + c menjadi StringBuilder internal otomatis. Gunakan StringBuilder hanya ketika jumlah operasi bertambah dinamis atau dalam loop dengan iterasi yang banyak.

Referensi Lanjutan #

Penutup #

Dengan StringBuilder, kita kini punya alat untuk menyusun teks panjang secara efisien. Namun untuk konten yang berbentuk banyak baris dengan struktur tertentu (JSON, SQL, HTML), StringBuilder tetap memerlukan perhatian terhadap indentasi dan karakter khusus. Solusi yang lebih alami adalah multiline raw string — topik yang akan kita bahas tuntas pada chapter Multiline Strings.