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()oruseLines()for large ones —readText()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 resources —bufferedReader().use {},inputStream().use {},printWriter().use {}guarantee the resource is closed even on exceptions. This is equivalent totry-finallywithout the boilerplate.writeText()overwrites,appendText()appends — always choose deliberately;writeText()erases the old content without warning.mkdirs()notmkdir()—mkdirs()creates all missing parent directories.mkdir()fails if the parent doesn’t exist.walk()for recursive iteration — cleaner than manual loops. UsewalkBottomUp()to delete directories (you must delete files before the directory).createTempFile()anddeleteOnExit()— for temporary files that must be cleaned up, always calldeleteOnExit()or delete manually in afinallyblock.kotlin.io.path.*for the Path API — Kotlin provides extensions onPathequivalent to theFileextensions:readText(),writeText(),exists(),extension,name. More modern thanjava.io.File.runCatching {}for fallible I/O operations — cleaner than verbose try-catch. Use.onSuccess,.onFailure,.getOrDefault,.getOrElseto handle the result.