Patterns for File I/O

NickJun 25, 2026

FIle I/O is a complex topic in most programming languages, and it feels like there are many rabbit holes to fall into when writing Kotlin code that targets the JVM.

For a start, you have all of the classes from java.io available to you. Then, on top of that, there is a whole different API provided via java.nio & java.nio.file. And if that wasn't enough, the Kotlin standard library adds a further layer on top. It's a lot to take in for students who are new to the language and who often don't have many years of programming experience.

I was wondering how to navigate this complexity as I start prepping teaching materials for the next academic year, but then I realised that a lot of it can be ignored; pretty much everything that our students are going to need can be done via the Path class from kotlin.io.path.

An instance of this class can be created via simply, with

val filePath = Path("test.txt")

Extension functions of Path then provide a simple, consistent approach to interacting with the specified file.

One-Liners

There are three 'one-liner' options for reading the entire contents of the file:

filePath.readText()    // returns a String
filePath.readLines()   // returns a List<String>
filePath.readBytes()   // returns a ByteArray

(Obviously, these are unsuitable for handling large files - see below.)

These are mirrored by equivalent functions for writing to the file:

filePath.writeText(text)     // writes a String
filePath.writeLines(lines)   // writes a List<String>
filePath.writeBytes(data)    // writes a ByteArray

By default, these will overwrite existing file contents - but there isn't a need to go into the details of opening files in append mode, because a comparable set of 'append' functions is available:

filePath.appendText(text)
filePath.appendLines(lines)
filePath.appendBytes(data)

It's hard to see how reading / writing small amounts of data could be made any simpler than this.

Reading & Writing Iteratively

Things get a little more complicated when reading or writing files a piece at a time, but not massively so. Reading text line-by-line, for example, can be achieved with

filePath.forEachLine {
    // line available as String object named 'it'
}

This is obviously an opportunity to start introducing the crucial concept of lambda expressions, although it isn't really necessary to know much about them to use this approach.

An alternative is

filePath.useLines {
    for (line in it) {
        // do something with line
    }
}

it here is a sequence: another important concept, to which this example might serve as a simple introduction.

Writing text files is not quite as straightforward, due to the need to first obtain a Writer object, but even then the amount of code needed is small:

filePath.writer().use {
    for (item in items) {
        it.write("$item\n")
    }
}

Here, the important thing to emphasize is how use() supports the proper closing of the file, avoiding any potential loss of data.

The unifying theme here is how important lambdas are to doing things simply and concisely in Kotlin. I think we should be able to get students using the patterns shown above without having to jump immediately into exactly what lambdas are and how they work.


Sign in to leave a note.