Reading Files
readText ยท readLines ยท useLinesKotlin 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.
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 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
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 ยท appendText ยท writeByteswriteText 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.
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
Closeable ยท try-with-resources equivalentAny 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.
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") } } }
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
bufferedReader ยท bufferedWriter ยท forEachLineAn 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.
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) }
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 ยท Files.* ยท modern alternativejava.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.
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") }
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
Syntax cheat-sheet| Function | Signature Shape | Notes |
|---|---|---|
| Read whole file as text | file.readText() | Loads everything into memory at once |
| Read whole file as lines | file.readLines() | List<String>; also loads everything at once |
| Stream lines lazily | file.useLines { seq -> ... } | Sequence only valid inside the block; auto-closes |
| Overwrite file contents | file.writeText(s) | Creates the file if it doesn't exist |
| Append to file | file.appendText(s) | Adds without touching existing content |
| Guaranteed cleanup | resource.use { ... } | Closes even if the block throws |
| Buffered reading | file.bufferedReader() | Batches disk reads for efficiency |
| Line-by-line, auto-closed | file.forEachLine { line -> ... } | Buffered internally, no explicit use{} needed |
| Modern path type | Path.of("a/b.txt") | Successor to File; better symlink/error handling |
| Path extensions | import kotlin.io.path.* | Adds File-like functions onto Path |