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 extensions — File.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 extensions —
File.readText(),File.useLines { }, etc. - Use
use { }for resource cleanup withCloseable. - Use
useLines/bufferedReader().usefor 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
ProcessBuilderfor 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.