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:
| Function | Mengembalikan | Memori | Cocok untuk |
|---|---|---|---|
readText() | String | Seluruh file | File < 10 MB |
readLines() | List<String> | Seluruh file | File kecil-sedang, butuh akses acak |
useLines { } | T (dari lambda) | Satu baris | File besar, streaming |
forEachLine { } | Unit | Satu baris | Iterasi tanpa return value |
bufferedReader() | BufferedReader | Sesuai buffer | Kontrol penuh via Java API |
Demo: Membaca File Biodata #
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:
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 = trueMari kita bedah bagian-bagian penting:
file.writeText(trimMargin): multiline string dengantrimMarginmenghapus indentasi dan karakter|di awal baris. Hasilnya empat baris bersih. File disimpan kebiodata.txt.- Bagian 1 (
readText): membaca seluruh file menjadi satuString. Outputseluruh.length = 54sama denganfile.length()— semua byte termuat. Cocok untuk file konfigurasi kecil. - Bagian 2 (
readLines): membaca baris per baris keList<String>. Baris terakhir yang kosong (dari newline di akhir) tidak disertakan. Empat baris bersih dikembalikan. - 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.mapNotNullmembuang baris yang tidak punya separator. - Bagian 4 (
useLines): lambda menerimaSequence<String>(lazy). Filtercontains("Kota")hanya cocok satu baris..toList()di akhir mengkonsumsi seluruh sequence dan menutup resource. - Bagian 5 (
useLines+sumOf): menghitung jumlah kata tanpa memuat seluruh file.Sequence.sumOfmemproses satu baris pada satu waktu. Untuk file log gigabyte, pola ini efisien. - Bagian 6 (
forEachLine): iterasi dengan side effect. Lambda menerima setiap baris. Tidak mengembalikan nilai — cocok untuk logging atau agregasi manual. - Bagian 7 (
bufferedReader): extension function yang membukaBufferedReaderJava langsung..use { }wajib agar stream ditutup.readLine()mengembalikan satu baris pertama ataunulljika file kosong. - 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. - Bagian 9 (file besar): menulis 100 baris dengan
bufferedWriter, lalu menghitung jumlah baris denganuseLines { it.count() }.count()padaSequencememproses 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:
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.
| Sistem | Karakter 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 #
- Kotlin stdlib kotlin.io
— paket
kotlin.ioyang menyediakanFile.readText,readLines, danuseLines - Kotlin stdlib java.io.file
— alias Kotlin stdlib untuk
java.io.Filedengan extension membaca teks
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.