IO #

The Kotlin Standard Library enriches the Java IO API with dozens of extension functions that make file operations far more concise and safe. Instead of writing the nested, boilerplate-heavy BufferedReader(FileReader(File(path))), Kotlin simplifies this to File(path).readText(). All these extension functions are available in the kotlin.io package and the auto-imported java.io.*. This article is a comprehensive reference to all the IO extensions available in the Kotlin Standard Library, from basic file operations, streaming, directories, to interoperability with the Java IO API.


Extension Functions on File #

Kotlin adds extension functions directly to java.io.File. No special import needed:

import java.io.File

val file = File("data.txt")

// File information
println(file.name)           // "data.txt"
println(file.nameWithoutExtension)  // "data"
println(file.extension)      // "txt"
println(file.path)           // relative or absolute path
println(file.absolutePath)   // full absolute path
println(file.canonicalPath)  // absolute path without symlinks

println(file.length())       // size in bytes
println(file.exists())       // true if it exists
println(file.isFile)         // true if a regular file
println(file.isDirectory)    // true if a directory
println(file.isHidden)       // true if hidden
println(file.canRead())      // true if readable
println(file.canWrite())     // true if writable

// Parent directory
println(file.parentFile)     // File object for the parent directory
println(file.parent)         // String path of the parent

// Resolve paths (join directory + name)
val dir = File("src/main/kotlin")
val subFile = dir.resolve("Main.kt")
println(subFile.path)  // src/main/kotlin/Main.kt

Reading Files #

Read the Entire Content #

val file = File("data.txt")

// readText() — read everything as a String (for small files)
val isi = file.readText()
val isiUtf8 = file.readText(Charsets.UTF_8)  // explicit encoding

// readLines() — read all lines as a List<String>
val baris = file.readLines()
baris.forEachIndexed { i, line -> println("${i + 1}: $line") }

// readBytes() — read as a ByteArray (for binary files)
val bytes = file.readBytes()
println("Size: ${bytes.size} bytes")

Read Line by Line (Streaming — for Large Files) #

// forEachLine — process one line at a time, doesn't load everything into memory
File("besar.csv").forEachLine { baris ->
    prosesBaris(baris)
}

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

// useLines — provides a Sequence that can be filtered/mapped lazily
File("besar.csv").useLines { sequence ->
    val total = sequence
        .drop(1)              // skip the header
        .filter { it.isNotBlank() }
        .mapNotNull { baris ->
            baris.split(",").getOrNull(2)?.toDoubleOrNull()
        }
        .sum()
    println("Total: $total")
}
// The file is automatically closed after the useLines block finishes

Read with a BufferedReader #

// bufferedReader() — more control, suitable for complex parsing
File("data.txt").bufferedReader().use { reader ->
    var baris: String?
    while (reader.readLine().also { baris = it } != null) {
        println(baris)
    }
}

// Or with readText() if you already have a reader
val reader = File("data.txt").bufferedReader()
val isi = reader.use { it.readText() }

Writing Files #

val file = File("output.txt")

// writeText() — write a string (OVERWRITES if it already exists)
file.writeText("First line\nSecond line\n")

// writeText() with an encoding
file.writeText("Hello, World!", Charsets.UTF_8)

// appendText() — APPEND to the end (doesn't overwrite)
file.appendText("Third line\n")
file.appendText("Fourth line\n", Charsets.UTF_8)

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

// appendBytes() — append bytes to the end of the file
File("biner.bin").appendBytes(byteArrayOf(0x21))  // '!'

Writing with a BufferedWriter and PrintWriter #

// bufferedWriter() — write many lines efficiently
File("log.txt").bufferedWriter().use { writer ->
    repeat(1000) { i ->
        writer.write("Log entry #$i: ${System.currentTimeMillis()}")
        writer.newLine()  // platform-appropriate newline
    }
}

// printWriter() — more convenient for text formatting
File("laporan.txt").printWriter().use { writer ->
    writer.println("=== SALES REPORT ===")
    writer.println()
    listOf("Laptop: 15.000.000", "Mouse: 250.000").forEach {
        writer.println("  $it")
    }
    writer.printf("%-20s %10s%n", "Product Name", "Price")
    writer.printf("%-20s %10.0f%n", "Gaming Laptop", 15_000_000.0)
}

Copying Files #

import java.io.File

// copyTo() — copy a file to a destination
val sumber = File("sumber.txt")
val tujuan = File("tujuan.txt")

sumber.copyTo(tujuan)                          // errors if the destination exists
sumber.copyTo(tujuan, overwrite = true)        // overwrite if it exists
sumber.copyTo(tujuan, bufferSize = 8192)       // custom buffer size

// copyRecursively() — copy an entire directory
File("direktori_sumber").copyRecursively(
    target = File("direktori_tujuan"),
    overwrite = true
)

// Source and destination as streams
File("gambar.png").inputStream().use { input ->
    File("gambar_backup.png").outputStream().use { output ->
        input.copyTo(output, bufferSize = 4096)
    }
}

Directory Operations #

val dir = File("proyek/data")

// Create directories
dir.mkdir()         // create one level (fails if the parent doesn't exist)
dir.mkdirs()        // create all parent levels at once

// List directory contents
dir.list()?.forEach { nama -> println(nama) }           // array of file names
dir.listFiles()?.forEach { file -> println(file.path) } // array of File objects

// Filter files in a directory
val fileKotlin = dir.listFiles { file -> file.extension == "kt" }
val fileKotlinBesar = dir.listFiles { _, nama -> nama.endsWith(".kt") }

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

// Only files, with a filter
val semuaKotlin = File("src").walk()
    .filter { it.isFile && it.extension == "kt" }
    .toList()
println("Kotlin files: ${semuaKotlin.size}")

// walk top-down vs bottom-up
File("src").walkTopDown().forEach { /* from root to leaf */ }
File("src").walkBottomUp().forEach { /* from leaf to root — useful for deletion */ }

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

// Delete a directory and its contents
File("temp").deleteRecursively()

// Delete a file (no error if it doesn't exist)
File("output.txt").delete()
File("tidak_ada.txt").deleteOnExit()  // delete when the JVM exits

Creating Temporary Files #

// createTempFile() — create a temporary file in the OS temp directory
val temp = createTempFile("prefix", ".txt")
println(temp.absolutePath)  // /tmp/prefix12345678.txt (Linux/Mac)

// createTempFile() with a custom directory
val tempDir = File("tmp")
tempDir.mkdirs()
val tempFile = createTempFile("data", ".csv", tempDir)

// The temporary file is automatically deleted when the JVM exits
temp.deleteOnExit()

// Use and delete manually
temp.use { file ->
    file.writeText("Temporary data")
    val isi = file.readText()
    println(isi)
}
// temp.delete()  // delete manually if not using deleteOnExit

// Create a temporary directory
val tempDirSistemt = createTempDir("temp-prefix")
tempDirSistemt.deleteRecursively()  // delete after finishing

InputStream and OutputStream #

Kotlin adds extension functions to the Java stream classes:

import java.io.*

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

// Read everything into a ByteArray
val bytes = inputStream.readBytes()

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

// Read text from an InputStream
val teks = File("data.txt").inputStream().bufferedReader().use { it.readText() }

// copyTo — copy an InputStream to an OutputStream
File("sumber.bin").inputStream().use { input ->
    File("tujuan.bin").outputStream().use { output ->
        val bytesSalin = input.copyTo(output)
        println("Copied: $bytesSalin bytes")
    }
}

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

// Read a resource from the JAR classpath
val resource = ClassLoader.getSystemResourceAsStream("config.properties")
    ?: throw IllegalStateException("Resource not found")

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

Reader and Writer #

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

// Read everything into a String
val isi = reader.use { it.readText() }

// Iterate lines
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()

// Write text
writer.use { w ->
    w.write("First content\n")
    w.write("Second content\n")
}

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

The Path API (Java NIO.2) with Kotlin #

Kotlin can also work with the more modern java.nio.file.Path:

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

// Creating a Path
val path = Path("data/config.yaml")              // Kotlin extension (Java 11+)
val pathOld = Paths.get("data", "config.yaml")   // the old Java way

// Information
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 if a regular file
println(path.isDirectory())  // true if a directory
println(path.fileSize())     // size in bytes

// Read/write operations (Java 11+)
val isi = path.readText()
path.writeText("new content")

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

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

// Copy, move, delete
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"))

// Create directories
Files.createDirectories(Path("deep/nested/dir"))

// Iterate a directory
Files.list(Path("src")).use { stream ->
    stream.forEach { p -> println(p.name) }
}

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

IO Error Handling #

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

// Pattern 1: runCatching for operations that may fail
fun bacaFileAman(path: String): Result<String> {
    return runCatching { File(path).readText() }
}

val hasil = bacaFileAman("config.txt")
hasil
    .onSuccess { isi -> println("Success: ${isi.length} characters") }
    .onFailure { e ->
        when (e) {
            is java.io.FileNotFoundException -> println("File not found: $path")
            is IOException -> println("I/O error: ${e.message}")
            is SecurityException -> println("Access denied: $path")
            else -> println("Unexpected error: ${e.message}")
        }
    }

// Default value on failure
val isi = bacaFileAman("config.txt").getOrDefault("# default configuration")
val isiElvis = bacaFileAman("config.txt").getOrElse { "" }

// Pattern 2: explicit try-catch for more control
fun tulisFileAman(path: String, konten: String): Boolean {
    return try {
        val file = File(path)
        file.parentFile?.mkdirs()  // create the parent directory if missing
        file.writeText(konten)
        true
    } catch (e: IOException) {
        println("Failed to write file: ${e.message}")
        false
    }
}

// Pattern 3: use{} ensures resources are always closed
fun prosesFile(path: String) {
    File(path).bufferedReader().use { reader ->
        // the reader is automatically closed even on exceptions
        reader.lineSequence().forEach { baris ->
            prosesBaris(baris)
        }
    }
}

// Validate before operating
fun bacaFileDenganValidasi(path: String): String {
    val file = File(path)
    require(file.exists()) { "File not found: $path" }
    require(file.isFile) { "Not a regular file: $path" }
    require(file.canRead()) { "No read permission: $path" }
    require(file.length() < 100 * 1024 * 1024) { "File too large (max 100MB)" }
    return file.readText()
}

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

Tips for Choosing the Right API #

flowchart TD
    A{I/O Goal?} --> B{Read a file?}
    B -- Yes --> C{File size?}
    C -->|Small < 10MB| D["readText()<br/>readLines()"]
    C -->|Large > 10MB| E["forEachLine()<br/>useLines { }"]
    A --> F{Write a file?}
    F -- Overwrite --> G["writeText()"]
    F -- Append --> H["appendText()"]
    F -- Many lines --> I["bufferedWriter()\nprintWriter()"]
    A --> J{Binary?}
    J -- Yes --> K["readBytes()\nwriteBytes()\ninputStream()\noutputStream()"]
    A --> L{Directory?}
    L -- Yes --> M["walk()\nlistFiles()\nmkdirs()"]

Summary #

  • readText() for small files, forEachLine() or useLines() for large onesreadText() loads the entire file into memory; dangerous for files of hundreds of MB. Use streaming for files that could be large.
  • Always use use {} for resourcesbufferedReader().use {}, inputStream().use {}, printWriter().use {} guarantee the resource is closed even on exceptions. This is equivalent to try-finally without the boilerplate.
  • writeText() overwrites, appendText() appends — always choose deliberately; writeText() erases the old content without warning.
  • mkdirs() not mkdir()mkdirs() creates all missing parent directories. mkdir() fails if the parent doesn’t exist.
  • walk() for recursive iteration — cleaner than manual loops. Use walkBottomUp() to delete directories (you must delete files before the directory).
  • createTempFile() and deleteOnExit() — for temporary files that must be cleaned up, always call deleteOnExit() or delete manually in a finally block.
  • kotlin.io.path.* for the Path API — Kotlin provides extensions on Path equivalent to the File extensions: readText(), writeText(), exists(), extension, name. More modern than java.io.File.
  • runCatching {} for fallible I/O operations — cleaner than verbose try-catch. Use .onSuccess, .onFailure, .getOrDefault, .getOrElse to handle the result.

← Previous: Strings   Next: Math →

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