Setelah membaca, sekarang menulis. Kotlin menyediakan extension function File.writeText untuk menulis keseluruhan, File.appendText untuk menambah tanpa menghapus, dan File.bufferedWriter() untuk kontrol penuh. Bab ini juga membahas pola atomic write — strategi menulis file konfigurasi penting tanpa risiko file setengah tertulis jika aplikasi crash di tengah operasi.

Extension Function Utama #

FunctionPerilakuCocok untuk
writeText(s)Overwrite seluruh fileFile kecil, konfigurasi
appendText(s)Tambah di akhirLog, append-only
writeBytes(b)Overwrite binerFile binary
bufferedWriter()Buka BufferedWriterPenulisan banyak baris
printWriter()Buka PrintWriterFormat teks dengan printf
outputStream()Buka OutputStreamKontrol byte-level

Demo: Beragam Cara Menulis #

kotlin
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStreamWriter
import java.nio.file.Files
import java.nio.file.StandardCopyOption

const val DEMO_DIR = "/tmp/kotlin-tutorial-tulis-teks"

fun main() {
    File(DEMO_DIR).deleteRecursively()
    File(DEMO_DIR).mkdirs()

    println("=== 1. writeText: tulis seluruh (overwrite) ===")
    val fileA = File("$DEMO_DIR/salam.txt")
    fileA.writeText("Halo dunia")
    println("fileA.length = ${fileA.length()} bytes")
    println("isi = ${fileA.readText()}")

    println()
    println("=== 2. writeText lagi: overwrite isi sebelumnya ===")
    fileA.writeText("Halo semuanya")  // ganti
    println("isi setelah writeText kedua = ${fileA.readText()}")

    println()
    println("=== 3. appendText: tambah tanpa menghapus ===")
    fileA.appendText("\nbaris kedua")
    fileA.appendText("\nbaris ketiga")
    println("isi setelah dua appendText:")
    println(fileA.readText().prependIndent("  "))

    println()
    println("=== 4. writeText dengan charset ===")
    val fileB = File("$DEMO_DIR/karakter.txt")
    fileB.writeText("Halo, Siti dari Bandung", Charsets.UTF_8)
    println("fileB (UTF-8).isi = ${fileB.readText()}")

    println()
    println("=== 5. bufferedWriter: tulis banyak baris dengan buffer ===")
    val fileC = File("$DEMO_DIR/log.txt")
    fileC.bufferedWriter().use { w ->
        repeat(3) { i ->
            w.write("log entry $i")
            w.newLine()
        }
    }
    println("fileC.lines = ${fileC.readLines()}")

    println()
    println("=== 6. bufferedWriter dengan append (via FileOutputStream) ===")
    FileOutputStream(fileC, true).bufferedWriter().use { w ->
        w.write("log entry tambahan")
        w.newLine()
    }
    println("isi setelah append:")
    println(fileC.readText().prependIndent("  "))

    println()
    println("=== 7. writeLines: tulis List<String> sekaligus ===")
    val fileD = File("$DEMO_DIR/daftar.txt")
    val items = listOf("apel", "mangga", "jeruk")
    fileD.writeText(items.joinToString("\n"))
    println("fileD.lines = ${fileD.readLines()}")

    println()
    println("=== 8. Pola atomic write: tulis ke file tmp, lalu move ===")
    val fileTarget = File("$DEMO_DIR/konfigurasi.txt")
    val fileTmp = File("$DEMO_DIR/konfigurasi.txt.tmp")
    // Tulis ke file sementara
    fileTmp.writeText("versi=2.0\nnama=Aplikasi Demo\n")
    // Pindahkan secara atomik
    Files.move(fileTmp.toPath(), fileTarget.toPath(),
        StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE)
    println("fileTarget.exists = ${fileTarget.exists()}")
    println("fileTmp.exists (harus false) = ${fileTmp.exists()}")
    println("fileTarget.isi:")
    println(fileTarget.readText().prependIndent("  "))

    println()
    println("=== 9. PrintWriter: format teks dengan newline otomatis ===")
    val fileE = File("$DEMO_DIR/laporan.txt")
    fileE.printWriter().use { pw ->
        pw.println("Laporan Harian")
        pw.println("==============")
        pw.printf("Tanggal: %s%n", "2026-01-15")
        pw.printf("Total: %d items%n", 42)
    }
    println("fileE.isi:")
    println(fileE.readText().prependIndent("  "))

    println()
    println("=== 10. OutputStreamWriter: kontrol charset eksplisit ===")
    val fileF = File("$DEMO_DIR/output.txt")
    FileOutputStream(fileF).use { fos ->
        OutputStreamWriter(fos, Charsets.UTF_8).use { w ->
            w.write("Halo dengan UTF-8 eksplisit")
        }
    }
    println("fileF.isi = ${fileF.readText()}")

    println()
    println("=== 11. Bersih-bersih ===")
    File(DEMO_DIR).deleteRecursively()
    println("demo dir dihapus = ${!File(DEMO_DIR).exists()}")
}

// Extension: OutputStream -> BufferedWriter via OutputStreamWriter
fun java.io.OutputStream.bufferedWriter(): java.io.BufferedWriter =
    java.io.BufferedWriter(OutputStreamWriter(this, Charsets.UTF_8))

Output yang Dihasilkan:

text
=== 1. writeText: tulis seluruh (overwrite) ===
fileA.length = 10 bytes
isi = Halo dunia

=== 2. writeText lagi: overwrite isi sebelumnya ===
isi setelah writeText kedua = Halo semuanya

=== 3. appendText: tambah tanpa menghapus ===
isi setelah dua appendText:
  Halo semuanya
  baris kedua
  baris ketiga

=== 4. writeText dengan charset ===
fileB (UTF-8).isi = Halo, Siti dari Bandung

=== 5. bufferedWriter: tulis banyak baris dengan buffer ===
fileC.lines = [log entry 0, log entry 1, log entry 2]

=== 6. bufferedWriter dengan append (via FileOutputStream) ===
isi setelah append:
  log entry 0
  log entry 1
  log entry 2
  log entry tambahan
  

=== 7. writeLines: tulis List<String> sekaligus ===
fileD.lines = [apel, mangga, jeruk]

=== 8. Pola atomic write: tulis ke file tmp, lalu move ===
fileTarget.exists = true
fileTmp.exists (harus false) = false
fileTarget.isi:
  versi=2.0
  nama=Aplikasi Demo
  

=== 9. PrintWriter: format teks dengan newline otomatis ===
fileE.isi:
  Laporan Harian
  ==============
  Tanggal: 2026-01-15
  Total: 42 items
  

=== 10. OutputStreamWriter: kontrol charset eksplisit ===
fileF.isi = Halo dengan UTF-8 eksplisit

=== 11. Bersih-bersih ===
demo dir dihapus = true

Mari kita bedah bagian-bagian penting:

  1. Bagian 1 (writeText): extension function yang membuka FileOutputStream (overwrite), menulis teks, lalu menutup. fileA.length = 10 bytes — panjang “Halo dunia” (10 karakter ASCII).
  2. Bagian 2 (overwrite): pemanggilan writeText kedua menghapus isi sebelumnya. Output membuktikan “Halo semuanya” menggantikan “Halo dunia”.
  3. Bagian 3 (appendText): dua pemanggilan menambah dua baris. \n di awal teks menandakan baris baru — penting karena appendText tidak menambah newline otomatis.
  4. Bagian 4 (charset): parameter kedua Charsets.UTF_8 eksplisit. Default sudah UTF-8, tetapi eksplisit membantu saat membaca file dari sumber luar.
  5. Bagian 5 (bufferedWriter + use { }): idiom untuk menulis banyak baris. w.newLine() menulis newline sesuai sistem operasi (Linux/macOS: \n, Windows: \r\n). .use { } menjamin writer ditutup meski ada exception.
  6. Bagian 6 (append via FileOutputStream): extension bufferedWriter() Kotlin default ke mode overwrite. Untuk append, buka FileOutputStream(file, true) dulu, lalu bungkus dengan OutputStreamWriter dan BufferedWriter. Extension function kustom di akhir file membungkus pola ini.
  7. Bagian 7 (joinToString("\n")): trik menulis List<String> ke file dengan newline antar elemen. Tidak ada method bawaan writeLines di Kotlin extension, jadi pola ini adalah idiom standar.
  8. Bagian 8 (atomic write): pola penting untuk konfigurasi. Tulis ke file sementara (konfigurasi.txt.tmp), lalu pindahkan secara atomik ke target. StandardCopyOption.ATOMIC_MOVE memastikan operasi move atomic — tidak ada kondisi di mana file target ada tetapi setengah ditulis. Jika aplikasi crash saat menulis file tmp, file target tetap utuh dengan versi lama.
  9. Bagian 9 (printWriter + printf): PrintWriter mendukung format string ala C (%s, %d, %n). Berguna untuk laporan terformat. %n menulis newline sistem (lebih portabel daripada hardcode \n).
  10. Bagian 10 (OutputStreamWriter eksplisit): pola Java klasik. FileOutputStream untuk byte, dibungkus OutputStreamWriter untuk karakter dengan charset, lalu BufferedWriter untuk efisiensi. Extension function File.writeText membungkus pola ini — di sini kita lakukan manual untuk kontrol penuh.

Pola Atomic Write: Mengapa Penting #

Pertimbangkan skenario: aplikasi sedang menulis konfigurasi 2 KB. Daya listrik mati saat 1 KB sudah tertulis. Tanpa atomic write, file konfigurasi sekarang korup — setengah versi lama, setengah versi baru. Aplikasi gagal membaca konfigurasinya sendiri saat startup.

Atomic write memecahkan masalah ini:

text
1. Tulis ke file.tmp          (file.tmp = data baru)
2. Files.move(file.tmp -> file, ATOMIC_MOVE)   (file = data baru, file.tmp hilang)

Sistem operasi menjamin Files.move dengan ATOMIC_MOVE adalah operasi tak terbagi: ia berhasil penuh atau gagal penuh. Tidak ada kondisi di mana file target kosong atau setengah ditulis.

Pola Append untuk Log File #

Untuk log file yang ingin di-append, gunakan FileOutputStream(file, true) atau File.appendText:

kotlin Pola append log dengan rotasi sederhana
val logFile = File("/var/log/app.log")
if (logFile.length() > MAX_LOG_SIZE) {
    logFile.renameTo(File("/var/log/app.log.old"))
}
logFile.appendText("[${waktuTetap}] pesan log\n")

Pola di atas melakukan log rotation sederhana — saat file melebihi ukuran tertentu, ganti nama ke .old, lalu mulai file baru. Log rotation produksi biasanya lebih kompleks (gzip file lama, batasi jumlah file lama), tetapi idiom dasarnya sama.

Referensi Lanjutan #

Selanjutnya #

Bab berikutnya, Buffered File Operations, akan membahas operasi berbuffer untuk file besar: BufferedReader, BufferedWriter, forEachLine, dan strategi memproses file log berukuran gigabyte tanpa memuat seluruhnya ke memori.