Membaca file teks adalah tugas paling umum dalam file I/O. Kotlin menyediakan beberapa extension function di java.io.File dengan trade-off yang berbeda: readText() untuk file kecil yang muat di memori, readLines() untuk akses per-baris, dan useLines { } untuk pemrosesan lazy tanpa memuat seluruh file. Bab ini membandingkan ketiganya serta menunjukkan pola pemrosesan umum: ekstraksi key-value, hitung jumlah kata, dan iterasi baris dengan side effect.

Tiga Pilihan Utama #

Pilihan extension function saat membaca file teks:

FunctionMengembalikanMemoriCocok untuk
readText()StringSeluruh fileFile < 10 MB
readLines()List<String>Seluruh fileFile kecil-sedang, butuh akses acak
useLines { }T (dari lambda)Satu barisFile besar, streaming
forEachLine { }UnitSatu barisIterasi tanpa return value
bufferedReader()BufferedReaderSesuai bufferKontrol penuh via Java API

Demo: Membaca File Biodata #

kotlin
import java.io.File

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

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

    // Siapkan file demo dengan beberapa baris
    val file = File("$DEMO_DIR/biodata.txt")
    file.writeText("""
        |Nama: Budi
        |Kota: Bandung
        |Pekerjaan: Guru
        |Hobi: Membaca
    """.trimMargin())
    println("file.path = ${file.name}")
    println("file.length = ${file.length()} bytes")
    println("file.content (raw):")
    println(file.readText().prependIndent("  "))

    println()
    println("=== 1. File.readText: baca seluruh sebagai String ===")
    val seluruh = file.readText()
    println("seluruh.length = ${seluruh.length}")
    println("seluruh.startsWith(\"Nama:\") = ${seluruh.startsWith("Nama:")}")

    println()
    println("=== 2. File.readLines: List<String>, per baris ===")
    val baris: List<String> = file.readLines()
    println("jumlah baris = ${baris.size}")
    baris.forEach { println("  - $it") }

    println()
    println("=== 3. readLines + map: ekstrak key-value ===")
    val mapBio = baris.mapNotNull { line ->
        val parts = line.split(": ", limit = 2)
        if (parts.size == 2) parts[0] to parts[1] else null
    }.toMap()
    println("mapBio = $mapBio")
    println("mapBio[\"Nama\"] = ${mapBio["Nama"]}")

    println()
    println("=== 4. useLines: pemrosesan lazily ===")
    val yangMengandungKota = file.useLines { lines ->
        lines.filter { it.contains("Kota") }.toList()
    }
    println("baris yang mengandung 'Kota' = $yangMengandungKota")

    println()
    println("=== 5. useLines: hitung jumlah kata tanpa memuat semua ke memori ===")
    val jumlahKata = file.useLines { lines ->
        lines.sumOf { it.split(" ").size }
    }
    println("jumlah kata = $jumlahKata")

    println()
    println("=== 6. forEachLine: iterasi dengan side effect ===")
    val panjangBaris = mutableListOf<Int>()
    file.forEachLine { line ->
        panjangBaris.add(line.length)
    }
    println("panjang tiap baris = $panjangBaris")

    println()
    println("=== 7. bufferedReader: pakai java.io.BufferedReader langsung ===")
    val barisPertama = file.bufferedReader().use { r -> r.readLine() }
    println("baris pertama = $barisPertama")

    println()
    println("=== 8. Baca dengan charset eksplisit ===")
    val fileUtf8 = File("$DEMO_DIR/utf8.txt")
    fileUtf8.writeText("Halo, nama Siti", Charsets.UTF_8)
    val isiUtf8 = fileUtf8.readText(Charsets.UTF_8)
    println("isi UTF-8 = $isiUtf8")

    println()
    println("=== 9. File besar: simulasi 100 baris ===")
    val fileBesar = File("$DEMO_DIR/besar.txt")
    fileBesar.bufferedWriter().use { w ->
        repeat(100) { i -> w.write("log baris ke-$i"); w.newLine() }
    }
    println("fileBesar.length = ${fileBesar.length()} bytes")
    val jumlahBarisBesar = fileBesar.useLines { it.count() }
    println("jumlah baris (useLines.count) = $jumlahBarisBesar")

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

Output yang Dihasilkan:

text
file.path = biodata.txt
file.length = 54 bytes
file.content (raw):
  Nama: Budi
  Kota: Bandung
  Pekerjaan: Guru
  Hobi: Membaca

=== 1. File.readText: baca seluruh sebagai String ===
seluruh.length = 54
seluruh.startsWith("Nama:") = true

=== 2. File.readLines: List<String>, per baris ===
jumlah baris = 4
  - Nama: Budi
  - Kota: Bandung
  - Pekerjaan: Guru
  - Hobi: Membaca

=== 3. readLines + map: ekstrak key-value ===
mapBio = {Nama=Budi, Kota=Bandung, Pekerjaan=Guru, Hobi=Membaca}
mapBio["Nama"] = Budi

=== 4. useLines: pemrosesan lazily ===
baris yang mengandung 'Kota' = [Kota: Bandung]

=== 5. useLines: hitung jumlah kata tanpa memuat semua ke memori ===
jumlah kata = 8

=== 6. forEachLine: iterasi dengan side effect ===
panjang tiap baris = [10, 13, 15, 13]

=== 7. bufferedReader: pakai java.io.BufferedReader langsung ===
baris pertama = Nama: Budi

=== 8. Baca dengan charset eksplisit ===
isi UTF-8 = Halo, nama Siti

=== 9. File besar: simulasi 100 baris ===
fileBesar.length = 1590 bytes
jumlah baris (useLines.count) = 100

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

Mari kita bedah bagian-bagian penting:

  1. file.writeText(trimMargin): multiline string dengan trimMargin menghapus indentasi dan karakter | di awal baris. Hasilnya empat baris bersih. File disimpan ke biodata.txt.
  2. Bagian 1 (readText): membaca seluruh file menjadi satu String. Output seluruh.length = 54 sama dengan file.length() — semua byte termuat. Cocok untuk file konfigurasi kecil.
  3. Bagian 2 (readLines): membaca baris per baris ke List<String>. Baris terakhir yang kosong (dari newline di akhir) tidak disertakan. Empat baris bersih dikembalikan.
  4. Bagian 3 (ekstrak key-value): pipeline fungsional mapNotNull { } + toMap(). split(": ", limit = 2) membagi sekali pada : , sehingga nilai yang mengandung : (mis. "Bandung: Jawa Barat") tidak rusak. mapNotNull membuang baris yang tidak punya separator.
  5. Bagian 4 (useLines): lambda menerima Sequence<String> (lazy). Filter contains("Kota") hanya cocok satu baris. .toList() di akhir mengkonsumsi seluruh sequence dan menutup resource.
  6. Bagian 5 (useLines + sumOf): menghitung jumlah kata tanpa memuat seluruh file. Sequence.sumOf memproses satu baris pada satu waktu. Untuk file log gigabyte, pola ini efisien.
  7. Bagian 6 (forEachLine): iterasi dengan side effect. Lambda menerima setiap baris. Tidak mengembalikan nilai — cocok untuk logging atau agregasi manual.
  8. Bagian 7 (bufferedReader): extension function yang membuka BufferedReader Java langsung. .use { } wajib agar stream ditutup. readLine() mengembalikan satu baris pertama atau null jika file kosong.
  9. Bagian 8 (charset eksplisit): default Charsets.UTF_8, tetapi bisa diganti. Penting saat membaca file dari sumber yang encodingnya tidak jelas — mis. file dari Windows yang sering memakai ISO-8859-1 atau UTF-16.
  10. Bagian 9 (file besar): menulis 100 baris dengan bufferedWriter, lalu menghitung jumlah baris dengan useLines { it.count() }. count() pada Sequence memproses baris satu per satu tanpa memuat semuanya ke memori.

useLines: Pola Lazy Paling Penting #

useLines { } adalah pola paling fleksibel dan aman resource. Lambda menerima Sequence<String>, dan resource (file handle) ditutup otomatis saat lambda selesai — bahkan jika ada exception. Pola umum:

kotlin Pola umum useLines
val hasil = file.useLines { lines ->
    lines
        .filter { it.startsWith("ERROR") }
        .map { it.substringAfter(": ") }
        .take(100)
        .toList()
}

Sequence memungkinkan chain operasi fungsional tanpa memuat seluruh file. take(100) menghentikan pemrosesan setelah 100 baris cocok — efisien untuk file besar.

Karakter Baris Baru #

Salah satu jebakan umum: baris baru berbeda antar sistem operasi.

SistemKarakter baris baru
Linux, macOS\n (LF)
Windows\r\n (CRLF)
Classic Mac OS\r (CR)

readLines() dan readText() mempertahankan karakter apa adanya di string. Jika file dibuat di Windows dan dibaca di Linux, \r mungkin masih ada di akhir setiap baris. Bersihkan dengan line.trimEnd('\r') atau gunakan line.trim() jika memang hanya ingin teks.

Referensi Lanjutan #

Selanjutnya #

Bab berikutnya, Menulis File Teks, akan membahas penulisan teks: File.writeText, File.appendText, File.bufferedWriter, serta pola atomic write yang aman untuk konfigurasi penting.