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")
Menulis Output #
// 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")
Menulis File #
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) dalamwithContext(Dispatchers.IO).Dispatchers.IOmenggunakan thread pool yang dioptimalkan untuk operasi blocking — thread-thread ini bisa menunggu tanpa memboroskan CPU thread dariDispatchers.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()atauuseLines()untuk file besar — jangan muat file ratusan MB ke memori sekaligus denganreadText(). Gunakan streaming untuk file yang ukurannya tidak pasti.use {}selalu untuk resource —File.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()bukanmkdir()—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.IOuntuk I/O di coroutine — jangan lakukan operasi file blocking diDispatchers.Default. Bungkus denganwithContext(Dispatchers.IO) { }.runCatching { }untuk operasi I/O yang bisa gagal — lebih bersih dari try-catch verbose. Gunakan.onSuccess,.onFailure,.getOrElse,.getOrDefault.PathAPI (NIO.2) untuk operasi advanced —Files.copy(),Files.move(),Files.deleteIfExists()lebih atomic dan lebih andal dariFileAPI lama untuk operasi file kritis.