Kotlin memperluas java.io.File dengan beragam extension function yang menyembunyikan boilerplate Java stream. Tanpa extension ini, membaca file di Java memerlukan FileInputStream → InputStreamReader → BufferedReader → try-with-resources. Dengan Kotlin, semuanya jadi satu baris: file.readText(). Bab ini membahas extension function paling sering dipakai beserta interop dengan stream Java dasar (InputStream, OutputStream, Reader, Writer).

Extension Function Utama #

Empat extension function paling sering dipakai di java.io.File:

FunctionMengembalikanKapan dipakai
readText()StringFile kecil, dibaca sekaligus
writeText(s)UnitMenulis teks (overwrite)
appendText(s)UnitMenambah teks di akhir
readLines()List<String>File per-baris, dimuat semua
useLines { }TPemrosesan baris lazily
forEachLine { }UnitIterasi baris tanpa List
readBytes()ByteArrayFile biner
writeBytes(b)UnitTulis biner
kotlin
import java.io.File

const val DEMO_DIR = "/tmp/kotlin-tutorial-java-io"

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

    println("=== 1. File.writeText: menulis teks ke file ===")
    val fileA = File("$DEMO_DIR/salam.txt")
    fileA.writeText("Halo dari Kotlin!")
    println("fileA.path = ${fileA.name}")
    println("fileA.length = ${fileA.length()} bytes")

    println()
    println("=== 2. File.readText: membaca seluruh isi sebagai String ===")
    val isi = fileA.readText()
    println("isi = $isi")

    println()
    println("=== 3. File.readLines: membaca baris per baris ===")
    val fileB = File("$DEMO_DIR/daftar.txt")
    fileB.writeText("Apel\nMangga\nJeruk\nPisang\n")
    val baris: List<String> = fileB.readLines()
    println("jumlah baris = ${baris.size}")
    baris.forEachIndexed { i, b -> println("  baris[${i}] = $b") }

    println()
    println("=== 4. File.appendText: menambah teks di akhir ===")
    fileB.appendText("Anggur\n")
    println("setelah appendText, jumlah baris = ${fileB.readLines().size}")

    println()
    println("=== 5. File.useLines: pemrosesan baris dengan resource aman ===")
    val hasilUpper = fileB.useLines { lines ->
        lines.map { it.uppercase() }.toList()
    }
    println("upper = $hasilUpper")

    println()
    println("=== 6. File.writeBytes / readBytes: data biner ===")
    val fileBin = File("$DEMO_DIR/data.bin")
    fileBin.writeBytes(byteArrayOf(0x48, 0x65, 0x6C, 0x6C, 0x6F))  // "Hello"
    val bytes: ByteArray = fileBin.readBytes()
    println("bytes (hex) = ${bytes.joinToString(" ") { "%02X".format(it) }}")
    println("bytes (string) = ${String(bytes)}")

    println()
    println("=== 7. InputStream / OutputStream: stream rendah (interop Java) ===")
    val fileStream = File("$DEMO_DIR/stream.txt")
    fileStream.outputStream().use { out ->
        out.write("baris dari stream".toByteArray())
    }
    val dariStream = fileStream.inputStream().use { it.readBytes() }
    println("dari stream = ${String(dariStream)}")

    println()
    println("=== 8. Reader / Writer: stream karakter ===")
    val fileWriter = File("$DEMO_DIR/karakter.txt")
    fileWriter.writer().use { w ->
        w.write("Halo karakter")
        w.write("\nbaris kedua")
    }
    val dariReader = fileWriter.reader().use { r -> r.readText() }
    println("dari reader = $dariReader")

    println()
    println("=== 9. forEachLine: iterasi baris tanpa memuat semua ke memori ===")
    val fileBig = File("$DEMO_DIR/besar.txt")
    fileBig.writeText((1..5).joinToString("\n") { "baris ke-$it" })
    val jumlahKata = mutableListOf<String>()
    fileBig.forEachLine { line ->
        jumlahKata.add(line)
    }
    println("jumlah baris diproses = ${jumlahKata.size}")
    println("baris terakhir = ${jumlahKata.last()}")

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

Output yang Dihasilkan:

text
=== 1. File.writeText: menulis teks ke file ===
fileA.path = salam.txt
fileA.length = 17 bytes

=== 2. File.readText: membaca seluruh isi sebagai String ===
isi = Halo dari Kotlin!

=== 3. File.readLines: membaca baris per baris ===
jumlah baris = 4
  baris[0] = Apel
  baris[1] = Mangga
  baris[2] = Jeruk
  baris[3] = Pisang

=== 4. File.appendText: menambah teks di akhir ===
setelah appendText, jumlah baris = 5

=== 5. File.useLines: pemrosesan baris dengan resource aman ===
upper = [APEL, MANGGA, JERUK, PISANG, ANGGUR]

=== 6. File.writeBytes / readBytes: data biner ===
bytes (hex) = 48 65 6C 6C 6F
bytes (string) = Hello

=== 7. InputStream / OutputStream: stream rendah (interop Java) ===
dari stream = baris dari stream

=== 8. Reader / Writer: stream karakter ===
dari reader = Halo karakter
baris kedua

=== 9. forEachLine: iterasi baris tanpa memuat semua ke memori ===
jumlah baris diproses = 5
baris terakhir = baris ke-5

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

Mari kita bedah bagian-bagian penting:

  1. Bagian 1 (writeText): extension function yang membuka FileOutputStream di dalam try-finally, menulis string, lalu menutup stream. length() mengembalikan 17 byte (panjang “Halo dari Kotlin!” = 17 karakter ASCII).
  2. Bagian 2 (readText): kebalikan dari writeText. Membaca seluruh file menjadi satu String. Hati-hati: file besar akan dimuat seluruhnya ke memori — tidak cocok untuk log file berukuran gigabyte.
  3. Bagian 3 (readLines): membaca file baris per baris dan mengembalikan List<String>. Baris terakhir yang kosong (dari \n di akhir) tidak disertakan. Output menunjukkan 4 baris buah, bukan 5.
  4. Bagian 4 (appendText): menambah teks tanpa menghapus yang ada. Setelah appendText("Anggur\n"), file memiliki 5 baris. Operasi ini membuka file dalam mode append.
  5. Bagian 5 (useLines { }): memproses baris secara lazy. Lambda menerima Sequence<String> yang dibaca satu per satu. Penting: Sequence hanya bisa dikonsumsi satu kali; jika butuh list, panggil .toList() di dalam blok. Setelah blok selesai, resource otomatis ditutup.
  6. Bagian 6 (readBytes / writeBytes): versi biner dari readText/writeText. Berguna untuk file gambar, data terkompresi, atau format biner lain. byteArrayOf(0x48, 0x65, ...) membentuk “Hello” dalam hex 48 65 6C 6C 6F.
  7. Bagian 7 (outputStream / inputStream): extension function yang membuka stream Java dasar. Pola .use { } wajib untuk memastikan stream ditutup. use adalah extension Kotlin pada Closeable yang setara dengan try-with-resources di Java.
  8. Bagian 8 (writer / reader): versi karakter dari stream. Writer dan Reader bekerja dengan Char, bukan byte. Cocok untuk file teks dengan encoding non-UTF-8 (default ke UTF-8 di Kotlin).
  9. Bagian 9 (forEachLine { }): iterasi baris dengan callback. Tidak memuat semua baris ke memori seperti readLines(), tetapi menghandle satu baris pada satu waktu. Cocok untuk file besar.

Hierarki Stream Java #

Memahami hierarki stream Java membantu saat membaca dokumentasi library yang masih Java-centric:

text
InputStream (byte stream, baca)
├── FileInputStream      (dari File)
├── BufferedInputStream  (buffered)
└── ...

OutputStream (byte stream, tulis)
├── FileOutputStream     (ke File)
├── BufferedOutputStream
└── ...

Reader (char stream, baca)
├── InputStreamReader    (decorator: InputStream -> Reader)
└── BufferedReader       (buffered, readLine())

Writer (char stream, tulis)
├── OutputStreamWriter
└── BufferedWriter

Extension function Kotlin seperti File.readText() dan File.readLines() membungkus kombinasi FileInputStream → InputStreamReader → BufferedReader di dalam satu pemanggilan.

Encoding dan Charset #

Secara default, extension function Kotlin memakai Charsets.UTF_8. Untuk encoding lain, berikan parameter eksplisit:

kotlin Membaca dengan charset ISO-8859-1
val isi = file.readText(Charsets.ISO_8859_1)
file.writeText(isi, Charsets.ISO_8859_1)

Referensi Lanjutan #

Selanjutnya #

Bab berikutnya, java.nio Interoperability, akan memperkenalkan API NIO.2 modern: Path, Files.newBufferedReader, Files.newBufferedWriter, dan Files.readAllLines. API ini direkomendasikan untuk kode baru karena fitur yang lebih kaya dan error handling yang lebih baik.