Polyglot
Languages Kotlin io
Kotlin § io

I/O

Kotlin’s I/O builds on the Java I/O foundation — java.io (stream-based), java.nio (channel-based, modern), and the substantial Kotlin extensions on File, BufferedReader, InputStream, OutputStream. The conventional Kotlin discipline favours the Kotlin extensionsFile.readText(), File.useLines { }, File.bufferedReader().use { } — admit substantial conciseness over Java’s verbose equivalents. The use extension on Closeable admits Java try-with-resources analogous behaviour. For standard I/O, println()/readLine() admit conventional console interaction. Async I/O via coroutines (treated separately) admits substantial non-blocking patterns. The combination — Java I/O substrate, Kotlin extensions for conciseness, the use for resource cleanup, the conventional console functions, the Sequence/Flow integration — is the substance of Kotlin’s I/O surface.

Console I/O

println("Hello, world!")                           // print + newline
print("no newline ")
print("then continue\n")

System.err.println("error message")                // to stderr

// Read input:
val line: String? = readLine()                    // reads from stdin
val n: Int = readLine()?.toIntOrNull() ?: 0

// Multiple lines:
val lines = generateSequence(::readLine).toList()  // until EOF

// Or in a loop:
while (true) {
    val input = readLine() ?: break
    process(input)
}

File reading

The principal Kotlin extensions:

import java.io.File

val file = File("/path/to/file.txt")

// Whole file as String:
val text = file.readText()
val text = file.readText(Charsets.UTF_8)

// As list of lines:
val lines: List<String> = file.readLines()

// As bytes:
val bytes: ByteArray = file.readBytes()

// Streaming line-by-line (for substantial files):
file.useLines { lines ->
    lines.filter { it.contains("ERROR") }
        .forEach { println(it) }
}

// Or with buffered reader:
file.bufferedReader().use { reader ->
    reader.lineSequence().forEach { line ->
        process(line)
    }
}

The useLines admits substantial efficient line-streaming with automatic resource cleanup.

File writing

val file = File("/path/to/file.txt")

// Whole file:
file.writeText("hello\nworld\n")
file.writeText("content", Charsets.UTF_8)

// Append:
file.appendText("more content\n")

// Bytes:
file.writeBytes(byteArrayOf(0x01, 0x02, 0x03))
file.appendBytes(moreBytes)

// Streaming:
file.bufferedWriter().use { writer ->
    items.forEach { item ->
        writer.write(item.toString())
        writer.newLine()
    }
}

// Or with PrintWriter:
file.printWriter().use { pw ->
    pw.println("Header")
    items.forEach { pw.println(it) }
}

use for resource cleanup

The use extension on Closeable admits automatic close():

val text = File("data.txt").bufferedReader().use { reader ->
    reader.readText()
}                                                  // reader closed automatically

// On exception, still closed:
file.bufferedWriter().use { writer ->
    writer.write("first")
    throw IOException()
    writer.write("second")                         // unreachable; writer still closed
}

The mechanism is conventional for any Closeable — files, streams, network connections, etc.

For multiple resources:

input.bufferedReader().use { reader ->
    output.bufferedWriter().use { writer ->
        reader.lineSequence().forEach { line ->
            writer.write(line.uppercase())
            writer.newLine()
        }
    }                                              // writer closed
}                                                  // reader closed

File system operations

val file = File("/path/to/file.txt")

file.exists()
file.isFile
file.isDirectory
file.isHidden
file.canRead()
file.canWrite()
file.canExecute()

file.length()                                      // size in bytes
file.lastModified()                                // milliseconds since epoch

file.absolutePath
file.canonicalPath
file.name                                          // "file.txt"
file.nameWithoutExtension                          // "file"
file.extension                                     // "txt"
file.parent                                        // String
file.parentFile                                    // File

// Manipulation:
file.delete()
file.renameTo(File("new.txt"))
file.createNewFile()
file.mkdir()
file.mkdirs()                                      // includes parent dirs
file.copyTo(File("dest.txt"), overwrite = true)
file.copyRecursively(File("dest"))
file.deleteRecursively()

Directory traversal

val dir = File("/path/to/dir")

// Direct children:
dir.listFiles()                                    // Array<File>?
dir.listFiles { _, name -> name.endsWith(".kt") }  // FilenameFilter
dir.listFiles { it.isFile && it.length() > 1000 }  // FileFilter

// Recursive:
dir.walk()                                         // FileTreeWalk
    .filter { it.isFile }
    .filter { it.extension == "kt" }
    .forEach { println(it) }

// Top-down or bottom-up:
dir.walkTopDown()
dir.walkBottomUp()                                  // useful for deletion

// Find first match:
dir.walk().find { it.name == "config.yaml" }

kotlin.io.path (Java NIO bridge)

For substantial control, Java NIO via Kotlin extensions:

import kotlin.io.path.*
import java.nio.file.Path

val path = Path("/path/to/file.txt")
val path = Path.of("dir", "subdir", "file.txt")    // platform-aware joining

path.exists()
path.isRegularFile()
path.isDirectory()
path.fileSize()
path.name
path.extension

// Read/write:
path.readText()
path.readLines()
path.readBytes()
path.writeText("content")
path.writeLines(listOf("line1", "line2"))
path.writeBytes(bytes)
path.appendText("more")

// Manipulation:
path.deleteIfExists()
path.copyTo(Path("dest"), overwrite = true)
path.createDirectory()
path.createDirectories()                            // includes parents

// Walking:
path.walk().filter { it.isRegularFile() }
path.listDirectoryEntries()                         // direct children

The NIO Path admits substantial cross-platform path handling — conventional for substantial file operations.

Streams

For substantial streaming I/O:

import java.io.*

// InputStream:
val input: InputStream = file.inputStream()
val bytes = input.readBytes()
val text = input.bufferedReader().readText()

// OutputStream:
val output: OutputStream = file.outputStream()
output.write(bytes)

// Conversion to/from bytes:
"hello".toByteArray(Charsets.UTF_8)                // ByteArray
String(byteArray, Charsets.UTF_8)                  // String

// Copying streams:
input.use { i -> output.use { o -> i.copyTo(o) } }

// Stream from URL:
URL("https://example.com").openStream().bufferedReader().use { it.readText() }

HTTP via Ktor or OkHttp

The standard library does not include a substantial HTTP client; the conventional choices:

Ktor

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*

val client = HttpClient(CIO)

suspend fun fetch(url: String): String {
    return client.get(url).bodyAsText()
}

// JSON:
import io.ktor.serialization.kotlinx.json.*
import io.ktor.client.plugins.contentnegotiation.*

val client = HttpClient(CIO) {
    install(ContentNegotiation) {
        json()
    }
}

@Serializable data class User(val id: Int, val name: String)

val user: User = client.get("https://api.example.com/users/1").body()

OkHttp

import okhttp3.*

val client = OkHttpClient()

val request = Request.Builder()
    .url("https://example.com")
    .build()

client.newCall(request).execute().use { response ->
    val body = response.body?.string()
    println(body)
}

Process I/O

For executing subprocesses:

val process = ProcessBuilder("ls", "-la")
    .redirectErrorStream(true)
    .start()

val output = process.inputStream.bufferedReader().readText()
val exitCode = process.waitFor()

// Or with kotlin extensions:
val process = Runtime.getRuntime().exec(arrayOf("ls", "-la"))
val output = process.inputStream.bufferedReader().readText()

// More substantial control:
val process = ProcessBuilder("git", "log")
    .directory(File("/repo"))
    .redirectErrorStream(true)
    .start()

process.inputStream.bufferedReader().use { reader ->
    reader.lineSequence().forEach { line ->
        println(line)
    }
}
process.waitFor()

JSON I/O

With kotlinx.serialization:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class User(val id: Int, val name: String, val email: String)

// Encode:
val user = User(1, "Alice", "alice@b.c")
val json = Json.encodeToString(user)

// Pretty:
val pretty = Json { prettyPrint = true }
val text = pretty.encodeToString(user)

// Decode:
val parsed = Json.decodeFromString<User>(json)

// File I/O:
val text = File("user.json").readText()
val user = Json.decodeFromString<User>(text)

File("output.json").writeText(Json.encodeToString(user))

Common patterns

Read whole file as string

val text = File("data.txt").readText()

Write whole file

File("output.txt").writeText("content")

Read line-by-line (substantial files)

File("large.log").useLines { lines ->
    lines.filter { it.contains("ERROR") }
        .forEach { println(it) }
}

Atomic file write

fun atomicWrite(path: String, content: String) {
    val tmp = File("$path.tmp")
    tmp.writeText(content)
    tmp.renameTo(File(path))                       // atomic replace
}

Walk directory

File("src").walk()
    .filter { it.isFile }
    .filter { it.extension == "kt" }
    .forEach { file -> println(file) }

File size and formatted

fun File.humanSize(): String = when {
    length() < 1024 -> "${length()} B"
    length() < 1024 * 1024 -> "${length() / 1024} KB"
    else -> "${length() / (1024 * 1024)} MB"
}

println(File("large.log").humanSize())

Reading config

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class Config(val host: String, val port: Int, val debug: Boolean = false)

fun loadConfig(path: String): Config {
    val text = File(path).readText()
    return Json.decodeFromString(text)
}

Streaming HTTP download

import java.net.URL

fun download(url: String, dest: File) {
    URL(url).openStream().use { input ->
        dest.outputStream().use { output ->
            input.copyTo(output)
        }
    }
}

download("https://example.com/file.zip", File("file.zip"))

Reading from process output

fun runCommand(vararg args: String): String {
    val process = ProcessBuilder(*args)
        .redirectErrorStream(true)
        .start()

    val output = process.inputStream.bufferedReader().readText()
    process.waitFor()
    return output
}

val gitStatus = runCommand("git", "status")

Writing CSV

File("output.csv").bufferedWriter().use { writer ->
    writer.write("id,name,email")
    writer.newLine()
    users.forEach { user ->
        writer.write("${user.id},${user.name},${user.email}")
        writer.newLine()
    }
}

Tail-style log reading

fun tailFile(file: File, callback: (String) -> Unit) {
    val raf = RandomAccessFile(file, "r")
    raf.seek(raf.length())                         // seek to end

    while (true) {
        val line = raf.readLine()
        if (line != null) {
            callback(line)
        } else {
            Thread.sleep(500)
        }
    }
}

Compressed file

import java.util.zip.*

// Read .gz:
GZIPInputStream(File("data.gz").inputStream()).use { gz ->
    gz.bufferedReader().readText()
}

// Write .gz:
GZIPOutputStream(File("output.gz").outputStream()).use { gz ->
    gz.bufferedWriter().use { writer ->
        writer.write(content)
    }
}

Reading from stdin

val lines = generateSequence(::readLine).toList()

// Or:
System.`in`.bufferedReader().use { reader ->
    reader.lineSequence().forEach { line ->
        process(line)
    }
}

The backticks around in admit using the keyword as an identifier (rare).

Async file I/O with coroutines

suspend fun loadFile(path: String): String = withContext(Dispatchers.IO) {
    File(path).readText()
}

suspend fun loadAll(paths: List<String>): List<String> = coroutineScope {
    paths.map { async(Dispatchers.IO) { File(it).readText() } }.awaitAll()
}

The withContext(Dispatchers.IO) admits substantial non-blocking file I/O via thread-pool offloading.

Reading config from environment

val dbUrl = System.getenv("DATABASE_URL") ?: "jdbc:postgres://localhost/db"
val apiKey = System.getenv("API_KEY") ?: error("API_KEY not set")

val port = System.getenv("PORT")?.toIntOrNull() ?: 8080

Properties file

import java.util.Properties

val props = Properties()
File("application.properties").inputStream().use { props.load(it) }

val host = props.getProperty("host", "localhost")
val port = props.getProperty("port", "8080").toInt()

Resources from classpath

val text = object {}.javaClass.getResourceAsStream("/data.json")
    ?.bufferedReader()?.readText()
    ?: error("data.json not found")

// Or:
val text = MyClass::class.java.getResource("/data.json")?.readText()
    ?: error("data.json not found")

The pattern admits accessing resources bundled in the JAR.

A note on the conventional discipline

The contemporary Kotlin I/O advice:

  • Use Kotlin extensionsFile.readText(), File.useLines { }, etc.
  • Use use { } for resource cleanup with Closeable.
  • Use useLines / bufferedReader().use for line-streaming substantial files.
  • Use walk() for directory traversal.
  • Use Path (java.nio) for substantial path manipulation.
  • Use kotlinx.serialization with file extensions for JSON I/O.
  • Use Ktor or OkHttp for HTTP.
  • Use withContext(Dispatchers.IO) for blocking I/O in coroutines.
  • Use ProcessBuilder for subprocesses.
  • Use environment variables / properties files for configuration.

The combination — Java I/O substrate, the substantial Kotlin extension surface, the use for resource management, the directory-walking integration with sequences, the kotlinx ecosystem for substantial protocols, the coroutine integration for async I/O — is the substance of Kotlin’s I/O surface. The discipline produces concise, safe, expressive I/O code with substantial built-in functionality and substantial integration with the JVM ecosystem.