File kecil dapat dimuat seluruhnya ke memori dengan readText() atau readLines(). File besar — log server berukuran gigabyte, CSV ratusan megabyte — harus diproses baris per baris dengan buffer. Bab ini membahas operasi buffer: BufferedReader, BufferedWriter, forEachLine, useLines, dan strategi meminimalkan konsumsi memori saat memproses file besar.
Mengapa Perlu Buffer #
Tanpa buffer, setiap read() atau write() langsung memanggil syscall ke sistem operasi. Untuk file log dengan satu juta baris, itu berarti satu juta syscall — sangat lambat. Buffer mengumpulkan data di memori (default 8 KB) dan menulis/membaca dalam batch besar, mengurangi syscall ke beberapa saja.
| Tanpa Buffer | Dengan Buffer |
|---|---|
| 1 syscall per baris | 1 syscall per 8 KB |
| Lambat untuk file besar | Cepat |
| Cocok untuk operasi tunggal | Cocok untuk banyak operasi |
Extension function Kotlin File.readText(), File.readLines(), File.writeText(), dan File.useLines { } sudah memakai buffer secara internal. Untuk kontrol penuh (ukuran buffer, mode append, charset), gunakan File.bufferedReader() dan File.bufferedWriter() langsung.
Demo: Memproses File Log Besar #
import java.io.File
import java.io.BufferedReader
import java.io.BufferedWriter
const val DEMO_DIR = "/tmp/kotlin-tutorial-buffered"
const val BIG_FILE_PATH = "$DEMO_DIR/log-besar.log"
fun main() {
File(DEMO_DIR).deleteRecursively()
File(DEMO_DIR).mkdirs()
println("=== 1. Buat file besar: 1000 baris dengan bufferedWriter ===")
val bigFile = File(BIG_FILE_PATH)
bigFile.bufferedWriter().use { w ->
repeat(1000) { i ->
w.write("2026-01-15 10:00:${"%02d".format(i % 60)} INFO event-$i")
w.newLine()
}
}
println("bigFile.length = ${bigFile.length()} bytes")
println("bigFile.exists = ${bigFile.exists()}")
println()
println("=== 2. BufferedReader: baca baris pertama dan kedua ===")
bigFile.bufferedReader().use { r ->
val baris1 = r.readLine()
val baris2 = r.readLine()
println("baris1 = $baris1")
println("baris2 = $baris2")
}
println()
println("=== 3. forEachLine: hitung jumlah baris tanpa List ===")
var jumlah = 0
bigFile.forEachLine { _ -> jumlah++ }
println("jumlah baris = $jumlah")
println()
println("=== 4. useLines: filter baris yang mengandung 'event-500' ===")
val yangCocok = bigFile.useLines { lines ->
lines.filter { it.contains("event-500") }.toList()
}
println("jumlah baris cocok = ${yangCocok.size}")
println("contoh = ${yangCocok.firstOrNull()}")
println()
println("=== 5. useLines + take: hanya 5 baris pertama ===")
val limaPertama = bigFile.useLines { lines ->
lines.take(5).toList()
}
println("5 baris pertama:")
limaPertama.forEach { println(" - $it") }
println()
println("=== 6. BufferedReader: baca batch (chunked) ===")
val batchPertama = bigFile.bufferedReader().use { r ->
val batch = mutableListOf<String>()
var dibaca = 0
while (dibaca < 3) {
val line = r.readLine() ?: break
batch.add(line)
dibaca++
}
batch
}
println("batch 3 baris pertama:")
batchPertama.forEach { println(" - $it") }
println()
println("=== 7. BufferedWriter dengan buffer size custom ===")
val fileBuf = File("$DEMO_DIR/buffered.txt")
val bufferSize = 8192 // 8 KB
java.io.FileOutputStream(fileBuf).use { fos ->
java.io.BufferedWriter(
java.io.OutputStreamWriter(fos, Charsets.UTF_8),
bufferSize
).use { w ->
repeat(5) { i ->
w.write("baris-$i")
w.newLine()
}
}
}
println("fileBuf.lines = ${fileBuf.readLines()}")
println()
println("=== 8. Bandingkan readLines vs useLines untuk file besar ===")
// Memori puncak: readLines muat semua ke List, useLines satu per satu
val jumlahReadLines = bigFile.readLines().size
val jumlahUseLines = bigFile.useLines { it.count() }
println("readLines -> $jumlahReadLines baris")
println("useLines -> $jumlahUseLines baris")
println("sama? ${jumlahReadLines == jumlahUseLines}")
println()
println("=== 9. Append dengan BufferedWriter (append=true) ===")
val fileAppend = File("$DEMO_DIR/append.txt")
fileAppend.writeText("baris awal\n")
java.io.FileOutputStream(fileAppend, true).use { fos ->
java.io.BufferedWriter(
java.io.OutputStreamWriter(fos, Charsets.UTF_8)
).use { w ->
w.write("baris tambahan via buffer")
w.newLine()
}
}
println("fileAppend.isi:")
println(fileAppend.readText().prependIndent(" "))
println()
println("=== 10. Bersih-bersih ===")
File(DEMO_DIR).deleteRecursively()
println("demo dir dihapus = ${!File(DEMO_DIR).exists()}")
}Output yang Dihasilkan:
=== 1. Buat file besar: 1000 baris dengan bufferedWriter ===
bigFile.length = 34890 bytes
bigFile.exists = true
=== 2. BufferedReader: baca baris pertama dan kedua ===
baris1 = 2026-01-15 10:00:00 INFO event-0
baris2 = 2026-01-15 10:00:01 INFO event-1
=== 3. forEachLine: hitung jumlah baris tanpa List ===
jumlah baris = 1000
=== 4. useLines: filter baris yang mengandung 'event-500' ===
jumlah baris cocok = 1
contoh = 2026-01-15 10:00:20 INFO event-500
=== 5. useLines + take: hanya 5 baris pertama ===
5 baris pertama:
- 2026-01-15 10:00:00 INFO event-0
- 2026-01-15 10:00:01 INFO event-1
- 2026-01-15 10:00:02 INFO event-2
- 2026-01-15 10:00:03 INFO event-3
- 2026-01-15 10:00:04 INFO event-4
=== 6. BufferedReader: baca batch (chunked) ===
batch 3 baris pertama:
- 2026-01-15 10:00:00 INFO event-0
- 2026-01-15 10:00:01 INFO event-1
- 2026-01-15 10:00:02 INFO event-2
=== 7. BufferedWriter dengan buffer size custom ===
fileBuf.lines = [baris-0, baris-1, baris-2, baris-3, baris-4]
=== 8. Bandingkan readLines vs useLines untuk file besar ===
readLines -> 1000 baris
useLines -> 1000 baris
sama? true
=== 9. Append dengan BufferedWriter (append=true) ===
fileAppend.isi:
baris awal
baris tambahan via buffer
=== 10. Bersih-bersih ===
demo dir dihapus = trueMari kita bedah bagian-bagian penting:
- Bagian 1 (menulis file besar):
bufferedWriter().use { }menulis 1000 baris. Total 34,890 byte. Buffer default 8 KB menampung beberapa baris sebelum flush ke disk — sangat cepat. - Bagian 2 (baca baris pertama/kedua):
BufferedReader.readLine()membaca satu baris pada satu waktu. MemanggilreadLine()dua kali mengembalikan baris pertama dan kedua. Cocok untuk membaca header file CSV atau log. - Bagian 3 (
forEachLine): iterasi dengan callback untuk setiap baris. Di dalamnya,forEachLinemembukaBufferedReaderdan menutupnya otomatis. Tidak mengembalikan nilai — cocok untuk agregasi manual seperti menghitung jumlah. - Bagian 4 (
useLines+ filter): filter baris yang mengandung"event-500". Sequence memproses satu baris pada satu waktu, sehingga memori puncak tetap kecil meski file berukuran gigabyte. Hasilnya hanya satu baris yang cocok — efisien. - Bagian 5 (
useLines+take(5)): idiom untuk membaca sebagian file tanpa memproses seluruhnya.take(5)menghentikan Sequence setelah 5 baris pertama — pipeline berhenti lebih awal, sangat cepat. - Bagian 6 (chunked read): pola membaca batch dengan
whileloop. Berguna saat perlu memproses baris dalam grup (mis. kirim batch ke database setiap 1000 baris). - Bagian 7 (custom buffer size): konstruktor
BufferedWriter(Writer, bufferSize)menerima ukuran buffer. Default 8192 byte (8 KB) cocok untuk kebanyakan kasus. Untuk file dengan baris panjang (mis. JSONLines besar), naikkan ke 64 KB. - Bagian 8 (perbandingan): keduanya menghitung jumlah baris dengan benar — 1000. Perbedaannya pada memori:
readLines()mengalokasikan List 1000 elemen di memori;useLines { it.count() }hanya menyimpan satu baris pada satu waktu. Untuk file 1 juta baris, perbedaan memori bisa 1 GB vs 1 KB. - Bagian 9 (append dengan buffer):
FileOutputStream(file, true)membuka stream append, lalu dibungkusOutputStreamWriterdanBufferedWriter. Pola lengkap untuk menulis log dengan buffer.
Strategi Memproses File Log #
Pola umum untuk file log server:
val logFile = File("/var/log/app.log")
val errors = logFile.useLines { lines ->
lines
.filter { it.contains("ERROR") }
.map { it.substringAfter("ERROR ") }
.toList()
}Untuk agregasi (mis. hitung error per jam):
val byHour = mutableMapOf<Int, Int>()
logFile.useLines { lines ->
lines.filter { it.contains("ERROR") }
.forEach { line ->
val hour = line.substring(11, 13).toInt() // posisi jam di timestamp
byHour[hour] = byHour.getOrDefault(hour, 0) + 1
}
}
println(byHour)Buffer Size: Berapa Besar #
Tabel panduan ukuran buffer:
| Ukuran | Cocok untuk |
|---|---|
4 KB (default Java BufferedReader) | File kecil, teks pendek |
| 8 KB (default Kotlin extension) | Umum — teks biasa |
| 64 KB | File log besar, CSV |
| 1 MB | File binary streaming, I/O berjaringan |
Buffer terlalu besar membuang memori; buffer terlalu kecil memperlambat I/O. Untuk log dan CSV, 64 KB sering sweet spot.
Referensi Lanjutan #
- Kotlin stdlib kotlin.io
— paket
kotlin.iountukuseLinesdan helper buffered - Java SE 21 java.io package
— ringkasan paket
java.ioJava SE 21 termasukBufferedReaderdanBufferedWriter
Selanjutnya #
Bab berikutnya, Path dan Directory, akan membahas operasi path dan direktori: Path.resolve, Path.relativize, File.list(), File.walk(), dan cara menelusuri struktur direktori secara rekursif.