Files & IO

File extensions for reading and writing, use for safe resource cleanup, buffered streams, and the Path API.

readText readLines useLines writeText use Path
๐Ÿ“–

Reading Files

Kotlin adds a set of extension functions directly onto Java's java.io.File, so no separate reader class or stream setup is needed for the common case of "just read the whole file." readText loads everything into one String; readLines loads everything into a List<String>, one entry per line. Both load the entire file into memory at once โ€” fine for typical config files and small-to-medium documents, wrong for anything large enough that holding it all in memory matters.

readText and readLines File extensions
import java.io.File

val file = File("config.txt")

// readText: whole file as one String
val contents: String = file.readText()
println(contents.length)

// readLines: whole file as a List, one entry per line
val lines: List<String> = file.readLines()
lines.forEach { println(it) }

// Both throw if the file doesn't exist โ€” no silent empty result
// File("missing.txt").readText()  // throws FileNotFoundException
useLines: streaming, memory-efficient reads useLines
// useLines streams line-by-line through a Sequence,
// never holding the whole file in memory at once โ€” the right
// choice for large files (see the Sequences guide for the
// lazy evaluation model this relies on)
val file = File("large_log.txt")

val errorCount = file.useLines { lines ->
    lines.count { it.contains("ERROR") }
}
println(errorCount)
// The Sequence passed to the block is only valid inside it โ€”
// the underlying file handle closes as soon as the block returns
๐Ÿ’ก
Default to readText/readLines for small files where simplicity wins. Switch to useLines once a file is large enough that loading it whole would be wasteful or risky โ€” log files, large datasets, anything where you only need to scan through once rather than hold the full contents.
โœ๏ธ

Writing Files

writeText overwrites a file's entire contents in one call, creating it if it doesn't exist; appendText adds to the end instead of replacing. Both are synchronous, whole-content operations โ€” for incremental writes over time, buffered streams (below) are the better fit.

writeText and appendText Writing
import java.io.File

val file = File("output.txt")

// writeText: overwrites the entire file (creates it if missing)
file.writeText("Line 1\n")

// appendText: adds to the end without touching existing content
file.appendText("Line 2\n")
file.appendText("Line 3\n")

println(file.readText())
// Line 1
// Line 2
// Line 3
๐Ÿ”’

use: Safe Resource Cleanup

Any Closeable or AutoCloseable โ€” streams, readers, database connections โ€” exposes a use extension function that runs a block and guarantees close() afterward, whether the block finishes normally or throws. It's Kotlin's answer to Java's try-with-resources, expressed as an ordinary function rather than special syntax.

Guaranteed close(), even on exception use
import java.io.File

// use { } closes the stream automatically, exactly once,
// after the block completes โ€” success or failure
File("data.txt").bufferedReader().use { reader ->
    var line = reader.readLine()
    while (line != null) {
        println(line)
        line = reader.readLine()
    }
}
// reader.close() has already run here, regardless of what happened above

// Nested resources: each use{} closes its own resource on the way out,
// innermost first, same as nested try-with-resources in Java
File("in.txt").bufferedReader().use { input ->
    File("out.txt").bufferedWriter().use { output ->
        input.forEachLine { output.write("$it\n") }
    }
}
๐Ÿ’ก
Wrap every manually-opened stream, reader, or writer in use { } โ€” there's essentially no reason not to. Most of the higher-level extensions like readText/writeText/useLines already call use internally, so you only reach for it explicitly once you drop down to a raw stream or reader yourself.
๐Ÿ“ถ

Buffered Streams

An unbuffered stream reads or writes to disk on every single call, which is slow when you're processing a file byte by byte or line by line. bufferedReader()/bufferedWriter() wrap a raw stream with an in-memory buffer, batching the actual disk I/O โ€” the same idea as Go's bufio.Reader/bufio.Writer. forEachLine combines buffering with iteration in one call for the common case.

forEachLine: buffered, line-by-line, one call forEachLine
import java.io.File

// forEachLine buffers internally and closes the stream when done โ€”
// no explicit use{} needed, it's handled for you
File("large_log.txt").forEachLine { line ->
    if (line.contains("ERROR")) println(line)
}
๐Ÿน
Coming from Go: Kotlin's bufferedReader()/bufferedWriter() map directly onto Go's bufio.NewReader/bufio.NewWriter โ€” both exist for the same reason, batching small reads/writes into fewer actual syscalls. forEachLine is the closer analogue of Go's bufio.Scanner loop, minus the manual for scanner.Scan() boilerplate.
๐Ÿ—‚

The Path API

java.nio.file.Path, together with the Files utility class, is Java's more capable successor to java.io.File โ€” better symlink handling, more granular error types, support for different filesystem providers (including in-memory or zip-backed filesystems). Kotlin doesn't wrap it with the same density of extension functions it gives File, but it composes fine, and for anything beyond simple read/write, Path is the more modern foundation to build on.

Path and Files, called from Kotlin Path / Files
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.*   // Kotlin's own Path extensions

val path: Path = Path.of("data/config.json")

// kotlin.io.path.* adds File-like extension functions onto Path too
val text = path.readText()
println(path.exists())
println(path.extension)   // "json"

// Files.walk for recursive directory traversal, as a Kotlin Sequence
val allJsonFiles = Files.walk(Path.of("data"))
    .toList()
    .filter { it.toString().endsWith(".json") }
โ„น๏ธ
For everyday scripts and small tools, File's extension functions are usually enough and read more concisely. Reach for Path/Files specifically when you need symbolic link control, atomic move/copy operations, or filesystem abstraction beyond the local disk.
๐Ÿ“‹

Quick Reference

Function Signature Shape Notes
Read whole file as textfile.readText()Loads everything into memory at once
Read whole file as linesfile.readLines()List<String>; also loads everything at once
Stream lines lazilyfile.useLines { seq -> ... }Sequence only valid inside the block; auto-closes
Overwrite file contentsfile.writeText(s)Creates the file if it doesn't exist
Append to filefile.appendText(s)Adds without touching existing content
Guaranteed cleanupresource.use { ... }Closes even if the block throws
Buffered readingfile.bufferedReader()Batches disk reads for efficiency
Line-by-line, auto-closedfile.forEachLine { line -> ... }Buffered internally, no explicit use{} needed
Modern path typePath.of("a/b.txt")Successor to File; better symlink/error handling
Path extensionsimport kotlin.io.path.*Adds File-like functions onto Path