IO

IO #

Kotlin Standard Library memperkaya Java IO API dengan puluhan extension function yang membuat operasi file jauh lebih ringkas dan aman. Daripada menulis BufferedReader(FileReader(File(path))) yang bertumpuk dan penuh boilerplate, Kotlin menyederhanakan ini menjadi File(path).readText(). Semua extension function ini tersedia di paket kotlin.io dan java.io.* yang sudah di-import secara otomatis. Artikel ini adalah referensi komprehensif semua extension IO yang tersedia di Kotlin Standard Library, dari operasi file dasar, streaming, direktori, hingga interoperabilitas dengan Java IO API.


Extension Function pada File #

Kotlin menambahkan extension function langsung pada java.io.File. Tidak perlu import khusus:

import java.io.File

val file = File("data.txt")

// Informasi file
println(file.name)           // "data.txt"
println(file.nameWithoutExtension)  // "data"
println(file.extension)      // "txt"
println(file.path)           // path relatif atau absolut
println(file.absolutePath)   // path absolut penuh
println(file.canonicalPath)  // path absolut tanpa symlink

println(file.length())       // ukuran dalam byte
println(file.exists())       // true jika ada
println(file.isFile)         // true jika file biasa
println(file.isDirectory)    // true jika direktori
println(file.isHidden)       // true jika tersembunyi
println(file.canRead())      // true jika bisa dibaca
println(file.canWrite())     // true jika bisa ditulis

// Direktori parent
println(file.parentFile)     // File objek untuk direktori parent
println(file.parent)         // String path parent

// Resolve path (gabung direktori + nama)
val dir = File("src/main/kotlin")
val subFile = dir.resolve("Main.kt")
println(subFile.path)  // src/main/kotlin/Main.kt

Membaca File #

Baca Seluruh Isi #

val file = File("data.txt")

// readText() — baca semua sebagai String (untuk file kecil)
val isi = file.readText()
val isiUtf8 = file.readText(Charsets.UTF_8)  // encoding eksplisit

// readLines() — baca semua baris sebagai List<String>
val baris = file.readLines()
baris.forEachIndexed { i, line -> println("${i + 1}: $line") }

// readBytes() — baca sebagai ByteArray (untuk file binary)
val bytes = file.readBytes()
println("Ukuran: ${bytes.size} byte")

Baca Baris Per Baris (Streaming — untuk File Besar) #

// forEachLine — proses satu baris setiap kali, tidak muat semua ke memori
File("besar.csv").forEachLine { baris ->
    prosesBaris(baris)
}

// forEachLine dengan encoding
File("latin.txt").forEachLine(Charsets.ISO_8859_1) { baris ->
    println(baris)
}

// useLines — memberikan Sequence yang bisa di-filter/map secara lazy
File("besar.csv").useLines { sequence ->
    val total = sequence
        .drop(1)              // lewati header
        .filter { it.isNotBlank() }
        .mapNotNull { baris ->
            baris.split(",").getOrNull(2)?.toDoubleOrNull()
        }
        .sum()
    println("Total: $total")
}
// File otomatis ditutup setelah blok useLines selesai

Baca dengan BufferedReader #

// bufferedReader() — lebih kontrol, cocok untuk parsing kompleks
File("data.txt").bufferedReader().use { reader ->
    var baris: String?
    while (reader.readLine().also { baris = it } != null) {
        println(baris)
    }
}

// Atau dengan readText() jika sudah punya reader
val reader = File("data.txt").bufferedReader()
val isi = reader.use { it.readText() }

val file = File("output.txt")

// writeText() — tulis string (MENIMPA jika sudah ada)
file.writeText("Baris pertama\nBaris kedua\n")

// writeText() dengan encoding
file.writeText("Halo, Dunia!", Charsets.UTF_8)

// appendText() — TAMBAHKAN ke akhir (tidak menimpa)
file.appendText("Baris ketiga\n")
file.appendText("Baris keempat\n", Charsets.UTF_8)

// writeBytes() — tulis ByteArray
val data = byteArrayOf(0x48, 0x65, 0x6C, 0x6C, 0x6F)  // "Hello"
File("biner.bin").writeBytes(data)

// appendBytes() — tambah byte ke akhir file
File("biner.bin").appendBytes(byteArrayOf(0x21))  // '!'
// bufferedWriter() — tulis banyak baris secara efisien
File("log.txt").bufferedWriter().use { writer ->
    repeat(1000) { i ->
        writer.write("Log entry #$i: ${System.currentTimeMillis()}")
        writer.newLine()  // newline yang platform-appropriate
    }
}

// printWriter() — lebih nyaman untuk format teks
File("laporan.txt").printWriter().use { writer ->
    writer.println("=== LAPORAN PENJUALAN ===")
    writer.println()
    listOf("Laptop: 15.000.000", "Mouse: 250.000").forEach {
        writer.println("  $it")
    }
    writer.printf("%-20s %10s%n", "Nama Produk", "Harga")
    writer.printf("%-20s %10.0f%n", "Laptop Gaming", 15_000_000.0)
}

Menyalin File #

import java.io.File

// copyTo() — salin file ke tujuan
val sumber = File("sumber.txt")
val tujuan = File("tujuan.txt")

sumber.copyTo(tujuan)                          // error jika tujuan sudah ada
sumber.copyTo(tujuan, overwrite = true)        // timpa jika sudah ada
sumber.copyTo(tujuan, bufferSize = 8192)       // custom buffer size

// copyRecursively() — salin seluruh direktori
File("direktori_sumber").copyRecursively(
    target = File("direktori_tujuan"),
    overwrite = true
)

// Sumber dan tujuan sebagai stream
File("gambar.png").inputStream().use { input ->
    File("gambar_backup.png").outputStream().use { output ->
        input.copyTo(output, bufferSize = 4096)
    }
}

Operasi Direktori #

val dir = File("proyek/data")

// Buat direktori
dir.mkdir()         // buat satu level (gagal jika parent tidak ada)
dir.mkdirs()        // buat semua level parent sekaligus

// Daftar isi direktori
dir.list()?.forEach { nama -> println(nama) }           // array nama file
dir.listFiles()?.forEach { file -> println(file.path) } // array File objek

// Filter file dalam direktori
val fileKotlin = dir.listFiles { file -> file.extension == "kt" }
val fileKotlinBesar = dir.listFiles { _, nama -> nama.endsWith(".kt") }

// Iterasi rekursif dengan walk()
File("src").walk().forEach { file ->
    println("${file.isFile.let { if (it) "FILE" else "DIR " }} ${file.path}")
}

// Hanya file, dengan filter
val semuaKotlin = File("src").walk()
    .filter { it.isFile && it.extension == "kt" }
    .toList()
println("File Kotlin: ${semuaKotlin.size}")

// walk dengan top-down vs bottom-up
File("src").walkTopDown().forEach { /* dari root ke leaf */ }
File("src").walkBottomUp().forEach { /* dari leaf ke root — berguna untuk hapus */ }

// Total ukuran direktori
val totalByte = File("src").walk()
    .filter { it.isFile }
    .sumOf { it.length() }
println("Total: ${totalByte / 1024} KB")

// Hapus direktori dan isinya
File("temp").deleteRecursively()

// Hapus file (tanpa error jika tidak ada)
File("output.txt").delete()
File("tidak_ada.txt").deleteOnExit()  // hapus saat JVM exit

Membuat File Sementara #

// createTempFile() — buat file sementara di direktori temp OS
val temp = createTempFile("prefix", ".txt")
println(temp.absolutePath)  // /tmp/prefix12345678.txt (Linux/Mac)

// createTempFile() dengan direktori kustom
val tempDir = File("tmp")
tempDir.mkdirs()
val tempFile = createTempFile("data", ".csv", tempDir)

// File sementara otomatis dihapus saat JVM exit
temp.deleteOnExit()

// Gunakan dan hapus manual
temp.use { file ->
    file.writeText("Data sementara")
    val isi = file.readText()
    println(isi)
}
// temp.delete()  // hapus manual jika tidak pakai deleteOnExit

// Buat direktori sementara
val tempDirSistemt = createTempDir("temp-prefix")
tempDirSistemt.deleteRecursively()  // hapus setelah selesai

InputStream dan OutputStream #

Kotlin menambahkan extension function pada Java stream classes:

import java.io.*

// InputStream extensions
val inputStream: InputStream = File("data.bin").inputStream()

// Baca semua ke ByteArray
val bytes = inputStream.readBytes()

// Baca dengan buffer
val buffer = ByteArray(1024)
inputStream.use { stream ->
    var bytesRead: Int
    while (stream.read(buffer).also { bytesRead = it } != -1) {
        prosesByte(buffer, 0, bytesRead)
    }
}

// Baca teks dari InputStream
val teks = File("data.txt").inputStream().bufferedReader().use { it.readText() }

// copyTo — salin InputStream ke OutputStream
File("sumber.bin").inputStream().use { input ->
    File("tujuan.bin").outputStream().use { output ->
        val bytesSalin = input.copyTo(output)
        println("Disalin: $bytesSalin byte")
    }
}

// OutputStream extensions
val outputStream: OutputStream = File("output.bin").outputStream()
outputStream.use { stream ->
    stream.write(byteArrayOf(1, 2, 3, 4, 5))
    stream.flush()
}

// Membaca resource dari JAR classpath
val resource = ClassLoader.getSystemResourceAsStream("config.properties")
    ?: throw IllegalStateException("Resource tidak ditemukan")

val config = resource.bufferedReader().use { it.readText() }
println(config)

Reader dan Writer #

// Reader extensions
val reader: Reader = File("data.txt").reader()
val bufferedReader: BufferedReader = File("data.txt").bufferedReader()

// Baca semua ke String
val isi = reader.use { it.readText() }

// Iterasi baris
bufferedReader.use { br ->
    br.lineSequence().forEach { baris ->
        println(baris)
    }
}

// Writer extensions
val writer: Writer = File("output.txt").writer()
val bufferedWriter: BufferedWriter = File("output.txt").bufferedWriter()

// Tulis teks
writer.use { w ->
    w.write("Konten pertama\n")
    w.write("Konten kedua\n")
}

// bufferedWriter dengan newLine()
bufferedWriter.use { bw ->
    bw.write("Baris 1")
    bw.newLine()
    bw.write("Baris 2")
    bw.newLine()
}

Path API (Java NIO.2) dengan Kotlin #

Kotlin juga bisa bekerja dengan java.nio.file.Path yang lebih modern:

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import kotlin.io.path.*

// Membuat Path
val path = Path("data/config.yaml")              // Kotlin extension (Java 11+)
val pathOld = Paths.get("data", "config.yaml")   // Java cara lama

// Informasi
println(path.name)           // "config.yaml" — extension property
println(path.nameWithoutExtension)  // "config"
println(path.extension)      // "yaml"
println(path.parent)         // data
println(path.isAbsolute)     // false
println(path.exists())       // true/false
println(path.isRegularFile()) // true jika file biasa
println(path.isDirectory())  // true jika direktori
println(path.fileSize())     // ukuran dalam byte

// Operasi baca/tulis (Java 11+)
val isi = path.readText()
path.writeText("konten baru")

val baris = path.readLines()
path.forEachLine { println(it) }

val bytes = path.readBytes()
path.writeBytes(bytes)

// Salin, pindah, hapus
Files.copy(Path("sumber.txt"), Path("tujuan.txt"), StandardCopyOption.REPLACE_EXISTING)
Files.move(Path("lama.txt"), Path("baru.txt"), StandardCopyOption.REPLACE_EXISTING)
Files.deleteIfExists(Path("hapus.txt"))

// Buat direktori
Files.createDirectories(Path("deep/nested/dir"))

// Iterasi direktori
Files.list(Path("src")).use { stream ->
    stream.forEach { p -> println(p.name) }
}

// Walk direktori rekursif
Files.walk(Path("src")).use { stream ->
    stream.filter { Files.isRegularFile(it) }
          .filter { it.extension == "kt" }
          .forEach { p -> println(p) }
}

Penanganan Error IO #

import java.io.File
import java.io.IOException

// Pola 1: runCatching untuk operasi yang mungkin gagal
fun bacaFileAman(path: String): Result<String> {
    return runCatching { File(path).readText() }
}

val hasil = bacaFileAman("config.txt")
hasil
    .onSuccess { isi -> println("Berhasil: ${isi.length} karakter") }
    .onFailure { e ->
        when (e) {
            is java.io.FileNotFoundException -> println("File tidak ditemukan: $path")
            is IOException -> println("Error I/O: ${e.message}")
            is SecurityException -> println("Akses ditolak: $path")
            else -> println("Error tidak terduga: ${e.message}")
        }
    }

// Nilai default jika gagal
val isi = bacaFileAman("config.txt").getOrDefault("# konfigurasi default")
val isiElvis = bacaFileAman("config.txt").getOrElse { "" }

// Pola 2: try-catch eksplisit untuk kontrol lebih
fun tulisFileAman(path: String, konten: String): Boolean {
    return try {
        val file = File(path)
        file.parentFile?.mkdirs()  // buat direktori parent jika belum ada
        file.writeText(konten)
        true
    } catch (e: IOException) {
        println("Gagal menulis file: ${e.message}")
        false
    }
}

// Pola 3: use{} memastikan resource selalu ditutup
fun prosesFile(path: String) {
    File(path).bufferedReader().use { reader ->
        // reader otomatis ditutup meski ada exception
        reader.lineSequence().forEach { baris ->
            prosesBaris(baris)
        }
    }
}

// Validasi sebelum operasi
fun bacaFileDenganValidasi(path: String): String {
    val file = File(path)
    require(file.exists()) { "File tidak ditemukan: $path" }
    require(file.isFile) { "Bukan file biasa: $path" }
    require(file.canRead()) { "Tidak punya izin baca: $path" }
    require(file.length() < 100 * 1024 * 1024) { "File terlalu besar (maks 100MB)" }
    return file.readText()
}

fun prosesBaris(baris: String) { println(baris) }

Tips Memilih API yang Tepat #

flowchart TD
    A{Tujuan I/O?} --> B{Baca file?}
    B -- Ya --> C{Ukuran file?}
    C -- Kecil < 10MB --> D["readText()\nreadLines()"]
    C -- Besar > 10MB --> E["forEachLine()\nuseLines { }"]
    A --> F{Tulis file?}
    F -- Menimpa --> G["writeText()"]
    F -- Menambahkan --> H["appendText()"]
    F -- Banyak baris --> I["bufferedWriter()\nprintWriter()"]
    A --> J{Binary?}
    J -- Ya --> K["readBytes()\nwriteBytes()\ninputStream()\noutputStream()"]
    A --> L{Direktori?}
    L -- Ya --> M["walk()\nlistFiles()\nmkdirs()"]

Ringkasan #

  • readText() untuk file kecil, forEachLine() atau useLines() untuk besarreadText() memuat seluruh file ke memori; berbahaya untuk file ratusan MB. Gunakan streaming untuk file yang ukurannya bisa besar.
  • use {} selalu untuk resourcebufferedReader().use {}, inputStream().use {}, printWriter().use {} menjamin resource ditutup meski terjadi exception. Ini setara dengan try-finally tanpa boilerplate.
  • writeText() menimpa, appendText() menambahkan — selalu pilih dengan sadar; writeText() menghapus isi lama tanpa peringatan.
  • mkdirs() bukan mkdir()mkdirs() membuat semua direktori parent yang belum ada. mkdir() gagal jika parent tidak ada.
  • walk() untuk iterasi rekursif — lebih bersih dari loop manual. Gunakan walkBottomUp() untuk hapus direktori (harus hapus file dulu sebelum direktori).
  • createTempFile() dan deleteOnExit() — untuk file sementara yang harus dibersihkan, selalu panggil deleteOnExit() atau hapus manual di finally.
  • kotlin.io.path.* untuk Path API — Kotlin menyediakan extension pada Path yang setara dengan extension pada File: readText(), writeText(), exists(), extension, name. Lebih modern dari java.io.File.
  • runCatching {} untuk operasi I/O yang bisa gagal — lebih bersih dari try-catch verbose. Gunakan .onSuccess, .onFailure, .getOrDefault, .getOrElse untuk menangani hasilnya.

← Sebelumnya: Strings   Berikutnya: Math →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact