Read and write files on the JVM with Kotlin’s concise extensions — readText and writeText for the simple cases, line-by-line processing for large files, and use for guaranteed resource cleanup.
Kotlin adds convenient extensions to java.io.File so common reads are one line: readText loads the whole file into a string, and readLines returns a list of lines. These are perfect for small-to-moderate files. For very large files you stream instead (next topic) to avoid loading everything into memory at once.
import java.io.File
fun main() {
val file = File("notes.txt")
val whole: String = file.readText() // entire file as one String
val lines: List<String> = file.readLines() // each line as a list element
println("${lines.size} lines, ${whole.length} chars")
// readText / readLines are ideal for small-to-moderate files.
// for huge files, stream line by line instead (next topic).
}Loading a multi-gigabyte file into a string would exhaust memory, so for large inputs you process one line at a time. forEachLine and useLines read lazily and close the file for you, keeping memory flat regardless of file size. Reaching for streaming over readText on unbounded input is the same "do not hold it all at once" instinct as sequences.
import java.io.File
fun main() {
var count = 0
// reads lazily, one line at a time, and CLOSES the file automatically
File("huge.log").forEachLine { line ->
if ("ERROR" in line) count++
}
println("errors: $count")
// useLines gives a Sequence you can chain — still lazy, still auto-closed
val firstError = File("huge.log").useLines { lines ->
lines.firstOrNull { "ERROR" in it }
}
// memory stays flat no matter how big the file is.
}Writing mirrors reading: writeText replaces a file’s contents with a string, and appendText adds to the end. Both create the file if it does not exist. For building up output incrementally you use a buffered writer; for a one-shot dump, writeText is the concise choice. Keep in mind writeText overwrites — use appendText when you mean to add.
import java.io.File
fun main() {
val file = File("out.txt")
file.writeText("first line\n") // creates/overwrites the file
file.appendText("second line\n") // adds to the end (doesn't overwrite)
// build up output with a buffered writer for many writes:
file.bufferedWriter().use { writer ->
repeat(3) { i -> writer.write("row $i\n") }
} // 'use' flushes + closes automatically (next topic)
println(file.readText())
}Anything that must be closed — a file stream, a socket, a database connection — should be wrapped in use, which runs your block and then closes the resource whether the block succeeds or throws. It is Kotlin’s equivalent of try-with-resources, and it prevents the leaked file handles and connections that plague manual close calls. Always reach for use with closeable resources.
import java.io.File
fun main() {
// 'use' closes the resource at the end — even if the block throws.
val firstLine = File("data.txt").bufferedReader().use { reader ->
reader.readLine() // do the work
} // reader.close() called automatically here
println(firstLine)
// manual open/close leaks handles when an exception skips close().
// 'use' guarantees cleanup — Kotlin's try-with-resources. Always use it
// for files, streams, sockets, and connections.
}