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 BufferDengan Buffer
1 syscall per baris1 syscall per 8 KB
Lambat untuk file besarCepat
Cocok untuk operasi tunggalCocok 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 #

kotlin
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:

text
=== 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 = true

Mari kita bedah bagian-bagian penting:

  1. 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.
  2. Bagian 2 (baca baris pertama/kedua): BufferedReader.readLine() membaca satu baris pada satu waktu. Memanggil readLine() dua kali mengembalikan baris pertama dan kedua. Cocok untuk membaca header file CSV atau log.
  3. Bagian 3 (forEachLine): iterasi dengan callback untuk setiap baris. Di dalamnya, forEachLine membuka BufferedReader dan menutupnya otomatis. Tidak mengembalikan nilai — cocok untuk agregasi manual seperti menghitung jumlah.
  4. 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.
  5. 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.
  6. Bagian 6 (chunked read): pola membaca batch dengan while loop. Berguna saat perlu memproses baris dalam grup (mis. kirim batch ke database setiap 1000 baris).
  7. 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.
  8. 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.
  9. Bagian 9 (append dengan buffer): FileOutputStream(file, true) membuka stream append, lalu dibungkus OutputStreamWriter dan BufferedWriter. Pola lengkap untuk menulis log dengan buffer.

Strategi Memproses File Log #

Pola umum untuk file log server:

kotlin Pola: ekstrak error dari log
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):

kotlin Pola: agregasi per kategori
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:

UkuranCocok untuk
4 KB (default Java BufferedReader)File kecil, teks pendek
8 KB (default Kotlin extension)Umum — teks biasa
64 KBFile log besar, CSV
1 MBFile 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 #

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.