I/O

I/O #

Input/Output (I/O) adalah fondasi dari hampir semua aplikasi yang berguna — membaca konfigurasi dari file, menulis log, memproses CSV, atau berinteraksi dengan pengguna di terminal. Kotlin menyederhanakan I/O secara signifikan dibanding Java: extension function seperti readText(), writeText(), forEachLine(), dan use {} menggantikan boilerplate bertumpuk yang biasa kamu tulis dengan BufferedReader, FileReader, dan try-finally manual. Artikel ini membahas semua aspek I/O di Kotlin secara mendalam — dari konsol, operasi file modern, penanganan file besar yang efisien, operasi direktori, hingga I/O asinkron dengan coroutine.

I/O Konsol #

Membaca Input #

// readLine() — membaca satu baris dari stdin, mengembalikan String?
print("Masukkan nama kamu: ")
val nama = readLine()           // String? — bisa null jika EOF
println("Halo, $nama!")

// Tangani kemungkinan null
val namaAman = readLine() ?: "Anonim"

// readLine() dengan konversi tipe
print("Masukkan umur: ")
val umur = readLine()?.toIntOrNull()
if (umur != null && umur >= 0) {
    println("Kamu berumur $umur tahun")
} else {
    println("Umur tidak valid")
}

// Membaca beberapa baris sekaligus (sampai EOF atau baris kosong)
val baris = mutableListOf<String>()
println("Masukkan teks (baris kosong untuk selesai):")
while (true) {
    val input = readLine() ?: break
    if (input.isBlank()) break
    baris.add(input)
}
println("Kamu memasukkan ${baris.size} baris")
// println — cetak dengan newline
println("Halo, Dunia!")

// print — cetak tanpa newline
print("Nama: ")
print("Budi")
println()  // newline manual

// Format output
val harga = 1_500_000.0
println("Harga: Rp%,.0f".format(harga))        // Harga: Rp1,500,000
System.out.printf("Harga: Rp%,.0f%n", harga)   // Alternatif Java printf

// Output ke stderr (untuk error/log)
System.err.println("Error: sesuatu tidak beres")

// Tabel sederhana di konsol
val header = "%-15s %5s %10s".format("Nama", "Umur", "Kota")
val pemisah = "-".repeat(35)
println(header)
println(pemisah)
listOf(
    Triple("Budi Santoso", 25, "Jakarta"),
    Triple("Sari Dewi", 28, "Bandung"),
    Triple("Ahmad Fauzi", 22, "Surabaya")
).forEach { (nama, umur, kota) ->
    println("%-15s %5d %10s".format(nama, umur, kota))
}

I/O File — Operasi Dasar #

Kotlin menyediakan extension function langsung di kelas File yang membuat operasi file jauh lebih ringkas dari Java.

Membaca File #

import java.io.File

val file = File("data.txt")

// readText() — baca seluruh isi sebagai String (untuk file kecil)
val isiLengkap = file.readText()
println(isiLengkap)

// readText() dengan encoding spesifik
val isiUtf8 = file.readText(Charsets.UTF_8)

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

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

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

// appendText() — tambahkan ke akhir file (tidak menimpa)
file.appendText("Baris ketiga\n")
file.appendText("Baris keempat\n")

// writeBytes() — tulis byte array (untuk file binary)
val dataBiner = byteArrayOf(0x48, 0x65, 0x6C, 0x6C, 0x6F)  // "Hello"
File("binary.bin").writeBytes(dataBiner)

// printWriter() — lebih fleksibel untuk format kompleks
File("laporan.txt").printWriter().use { writer ->
    writer.println("=== Laporan Penjualan ===")
    writer.println()
    listOf("Laptop: Rp15.000.000", "Mouse: Rp250.000").forEach {
        writer.println(it)
    }
    writer.printf("Total item: %d%n", 2)
}

// bufferedWriter() — efisien untuk banyak operasi tulis kecil
File("log.txt").bufferedWriter().use { writer ->
    repeat(1000) { i ->
        writer.write("Log entry #$i")
        writer.newLine()
    }
}

Memilih Metode Baca yang Tepat #

flowchart TD
    A{Ukuran file?} --> B{Kecil\n< 10MB}
    B -- Ya --> C{Butuh\nsemua baris?}
    C -- Ya --> D["readLines()\nList<String>"]
    C -- Tidak --> E["readText()\nString tunggal"]
    A --> F{Besar\n> 10MB}
    F -- Ya --> G{Perlu proses\ntiap baris?}
    G -- Ya --> H["forEachLine { }\nStreaming — hemat memori"]
    G -- Tidak --> I["useLines { }\nSequence — lazy"]
    A --> J{File binary\nbukan teks}
    J -- Ya --> K["readBytes() /\nInputStream"]

Menangani File Besar secara Efisien #

Untuk file besar, readText() dan readLines() memuat seluruh file ke memori — berbahaya untuk file ratusan MB. Gunakan pendekatan streaming:

val fileBesar = File("data-besar.csv")

// ANTI-PATTERN: muat semua ke memori
val semuaBaris = fileBesar.readLines()  // ✗ berbahaya untuk file besar!
semuaBaris.forEach { prosesBaris(it) }

// BENAR: forEachLine — proses baris per baris tanpa muat ke memori
fileBesar.forEachLine { baris ->
    prosesBaris(baris)
}

// BENAR: useLines — memberikan Sequence yang bisa di-filter/map secara lazy
fileBesar.useLines { sequence ->
    val totalPenjualan = sequence
        .drop(1)  // lewati header
        .filter { it.isNotBlank() }
        .map { baris ->
            val kolom = baris.split(",")
            kolom.getOrNull(2)?.toDoubleOrNull() ?: 0.0
        }
        .sum()
    println("Total penjualan: Rp${"%,.0f".format(totalPenjualan)}")
}
// Sequence otomatis ditutup setelah blok useLines selesai

Membaca CSV Baris per Baris #

data class Produk(val id: Int, val nama: String, val harga: Double, val stok: Int)

fun bacaCSV(path: String): List<Produk> {
    val hasil = mutableListOf<Produk>()

    File(path).useLines { baris ->
        baris
            .drop(1)  // lewati header: id,nama,harga,stok
            .filter { it.isNotBlank() }
            .forEach { baris ->
                val kolom = baris.split(",")
                runCatching {
                    hasil.add(Produk(
                        id    = kolom[0].trim().toInt(),
                        nama  = kolom[1].trim(),
                        harga = kolom[2].trim().toDouble(),
                        stok  = kolom[3].trim().toInt()
                    ))
                }.onFailure { e ->
                    System.err.println("Baris tidak valid: '$baris' — ${e.message}")
                }
            }
    }

    return hasil
}

Path API — Cara Modern (Java NIO.2) #

java.nio.file.Path adalah API file modern yang lebih powerful dari java.io.File, dengan dukungan symlink, permission, dan operasi atomik.

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption

// Membuat Path
val pathFile = Path.of("data/config.yaml")  // Java 11+
val pathLama = Paths.get("data", "config.yaml")  // Java 8+

// Info file
println(Files.exists(pathFile))           // true/false
println(Files.isDirectory(pathFile))      // false
println(Files.isReadable(pathFile))       // true/false
println(Files.size(pathFile))             // ukuran dalam byte

// Membaca dengan Path
val isi = Files.readString(pathFile)      // Java 11+
val semuaBaris = Files.readAllLines(pathFile)

// Menulis dengan Path
Files.writeString(pathFile, "konten baru")  // Java 11+
Files.write(pathFile, listOf("baris1", "baris2"))

// Salin file
Files.copy(
    Path.of("sumber.txt"),
    Path.of("tujuan.txt"),
    StandardCopyOption.REPLACE_EXISTING
)

// Pindahkan/rename file
Files.move(
    Path.of("lama.txt"),
    Path.of("baru.txt"),
    StandardCopyOption.REPLACE_EXISTING
)

// Hapus file
Files.deleteIfExists(Path.of("hapus.txt"))

Operasi Direktori #

import java.io.File

val direktori = File("proyek/data")

// Buat direktori (termasuk semua parent yang belum ada)
direktori.mkdirs()

// Daftar isi direktori
direktori.listFiles()?.forEach { file ->
    val tipe = if (file.isDirectory) "[DIR]" else "[FILE]"
    println("$tipe ${file.name} (${file.length()} byte)")
}

// Filter berdasarkan ekstensi
val fileKotlin = direktori.listFiles { file ->
    file.extension == "kt"
} ?: emptyArray()
println("File Kotlin: ${fileKotlin.size}")

// Walk — iterasi rekursif seluruh subdirektori
File("src").walk()
    .filter { it.isFile && it.extension == "kt" }
    .forEach { file ->
        println("${file.relativeTo(File("src")).path}: ${file.length()} byte")
    }

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

// Hapus direktori dan seluruh isinya
fun hapusRekursif(dir: File): Boolean {
    dir.walk().sortedDescending().forEach { it.delete() }
    return !dir.exists()
}

Serialisasi ke File — JSON dan Properties #

Menyimpan dan Membaca Properties #

import java.util.Properties

// Menulis properties
val props = Properties()
props.setProperty("host", "localhost")
props.setProperty("port", "5432")
props.setProperty("dbName", "myapp")

File("config.properties").outputStream().use { out ->
    props.store(out, "Konfigurasi Aplikasi")
}

// Membaca properties
val konfigurasi = Properties()
File("config.properties").inputStream().use { input ->
    konfigurasi.load(input)
}

println(konfigurasi.getProperty("host"))    // localhost
println(konfigurasi.getProperty("port"))    // 5432
println(konfigurasi.getProperty("dbName", "default"))  // myapp (dengan default)

Serialisasi Manual ke JSON-like Format #

data class Konfigurasi(
    val host: String,
    val port: Int,
    val debug: Boolean,
    val tags: List<String>
)

// Simpan ke file sebagai representasi teks
fun Konfigurasi.simpanKe(file: File) {
    file.printWriter().use { w ->
        w.println("{")
        w.println("  \"host\": \"$host\",")
        w.println("  \"port\": $port,")
        w.println("  \"debug\": $debug,")
        w.println("  \"tags\": ${tags.joinToString(", ", "[\"", "\"]")}")
        w.println("}")
    }
}

Stream I/O — Operasi Binary #

Untuk file binary (gambar, PDF, ZIP), gunakan InputStream dan OutputStream:

import java.io.File

// Salin file binary dengan use{}
fun salinFileBinary(sumber: String, tujuan: String) {
    File(sumber).inputStream().use { input ->
        File(tujuan).outputStream().use { output ->
            input.copyTo(output, bufferSize = 8192)  // buffer 8KB
        }
    }
}

// Baca resource dari classpath (untuk aplikasi yang dikemas dalam JAR)
fun bacaResource(namaFile: String): String {
    return Thread.currentThread()
        .contextClassLoader
        .getResourceAsStream(namaFile)
        ?.bufferedReader()
        ?.use { it.readText() }
        ?: throw IllegalArgumentException("Resource tidak ditemukan: $namaFile")
}

// Compress dengan GZip
fun kompresFile(input: String, output: String) {
    java.util.zip.GZIPOutputStream(File(output).outputStream()).use { gzip ->
        File(input).inputStream().use { it.copyTo(gzip) }
    }
}

fun dekompresiFile(input: String, output: String) {
    java.util.zip.GZIPInputStream(File(input).inputStream()).use { gzip ->
        File(output).outputStream().use { gzip.copyTo(it) }
    }
}

I/O Asinkron dengan Coroutine #

Untuk aplikasi yang membutuhkan I/O non-blocking, jalankan operasi I/O di Dispatchers.IO:

import kotlinx.coroutines.*
import java.io.File

// Baca file secara asinkron
suspend fun bacaFileAsync(path: String): String {
    return withContext(Dispatchers.IO) {
        File(path).readText()
    }
}

// Tulis file secara asinkron
suspend fun tulisFileAsync(path: String, konten: String) {
    withContext(Dispatchers.IO) {
        File(path).writeText(konten)
    }
}

// Proses banyak file secara paralel
suspend fun prosesSemuaFile(direktori: String): Map<String, Int> {
    return withContext(Dispatchers.IO) {
        File(direktori).listFiles { f -> f.extension == "txt" }
            ?.map { file ->
                async {
                    file.name to file.readLines().size
                }
            }
            ?.awaitAll()
            ?.toMap()
            ?: emptyMap()
    }
}

fun main() = runBlocking {
    // Baca beberapa file secara paralel
    val file1 = async { bacaFileAsync("data1.txt") }
    val file2 = async { bacaFileAsync("data2.txt") }
    val file3 = async { bacaFileAsync("data3.txt") }

    val hasil = listOf(file1, file2, file3).awaitAll()
    println("Berhasil membaca ${hasil.size} file")

    // Proses semua file .txt dalam direktori
    val jumlahBaris = prosesSemuaFile("data/")
    jumlahBaris.forEach { (nama, jumlah) ->
        println("$nama: $jumlah baris")
    }
}
Selalu jalankan operasi I/O file yang blocking (baca/tulis disk) dalam withContext(Dispatchers.IO). Dispatchers.IO menggunakan thread pool yang dioptimalkan untuk operasi blocking — thread-thread ini bisa menunggu tanpa memboroskan CPU thread dari Dispatchers.Default.

Penanganan Error I/O #

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

// Pendekatan idiomatis dengan runCatching
fun bacaFileAman(path: String): Result<String> {
    return runCatching { File(path).readText() }
}

// Penggunaan
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}")
            else -> println("Error tidak terduga: ${e.message}")
        }
    }

// Atau dengan getOrElse untuk nilai default
val isi = bacaFileAman("config.txt").getOrElse { "" }

// Validasi sebelum operasi
fun tulisFileAman(path: String, konten: String): Boolean {
    val file = File(path)

    // Pastikan direktori parent ada
    file.parentFile?.mkdirs()

    return runCatching {
        file.writeText(konten)
        true
    }.getOrDefault(false)
}

Ringkasan #

  • readText() untuk file kecil, forEachLine() atau useLines() untuk file besar — jangan muat file ratusan MB ke memori sekaligus dengan readText(). Gunakan streaming untuk file yang ukurannya tidak pasti.
  • use {} selalu untuk resourceFile.bufferedReader().use { }, inputStream().use { }, printWriter().use { }. Kotlin menjamin resource ditutup meski terjadi exception.
  • appendText() untuk menambah, writeText() untuk menimpa — pilih dengan sadar; writeText() menghapus isi lama tanpa peringatan.
  • mkdirs() bukan mkdir()mkdirs() membuat semua direktori parent yang belum ada sekaligus. mkdir() gagal jika parent belum ada.
  • File.walk() untuk iterasi rekursif — lebih bersih dari loop manual. Filter dengan .filter { it.isFile } dan proses dengan .forEach { }.
  • Dispatchers.IO untuk I/O di coroutine — jangan lakukan operasi file blocking di Dispatchers.Default. Bungkus dengan withContext(Dispatchers.IO) { }.
  • runCatching { } untuk operasi I/O yang bisa gagal — lebih bersih dari try-catch verbose. Gunakan .onSuccess, .onFailure, .getOrElse, .getOrDefault.
  • Path API (NIO.2) untuk operasi advancedFiles.copy(), Files.move(), Files.deleteIfExists() lebih atomic dan lebih andal dari File API lama untuk operasi file kritis.

← Sebelumnya: Multi Threading   Berikutnya: Socket →

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