Leer y escribir archivos en Kotlin: readText, useLines, use y Path

Kotlin usa las clases de archivos de Java, pero les añade funciones que convierten cada tarea habitual en una línea: leer un archivo entero, escribir, añadir al final o recorrer un registro enorme sin cargarlo en memoria. Aquí están todas, con el error que más se comete con cada una.

Escribir y leer un archivo pequeño

La biblioteca estándar de Kotlin añade a java.io.File funciones de extensión que hacen de cada operación habitual una sola línea. writeText crea el archivo (o lo vacía si ya existía), appendText añade al final y readText lo lee entero:

import java.io.File

fun main() {
    val notas = File("notas.txt")
    notas.writeText("Comprar café\nLlamar a Luis\n")
    notas.appendText("Pagar la luz\n")

    print(notas.readText())
    val lineas = notas.readLines()
    println("${lineas.size} líneas; la segunda es «${lineas[1]}»")
    println("${notas.readText().length} caracteres, ${notas.length()} bytes")
}
salidaComprar café
Llamar a Luis
Pagar la luz
3 líneas; la segunda es «Llamar a Luis»
40 caracteres, 41 bytes

Todas usan UTF-8 si no les dices otra cosa, en cualquier sistema operativo: por eso «café» ocupa un byte más de lo que mide como texto (la «é» son dos bytes en UTF-8). Si tienes que leer un archivo viejo de Windows con otra codificación, pásala: readText(Charsets.ISO_8859_1). Y no hay que cerrar nada: cada función abre, hace su trabajo y cierra.

readText() y readLines() cargan el archivo entero en memoria. Para una configuración o una lista corta, perfecto; para un registro de varios gigas, no.

Archivos grandes: useLines, línea a línea

useLines abre el archivo, te da sus líneas como una Sequence perezosa (se leen conforme las pides, nunca todas a la vez) y lo cierra al terminar el bloque. Este programa saca los errores de un registro de servidor:

import java.io.File

fun main() {
    val errores = File("servidor.log").useLines { lineas ->
        lineas.filter { " ERROR " in it }
            .map { it.substringAfter(" ERROR ") }
            .toList()
    }
    println("${errores.size} errores:")
    errores.forEach { println("  - $it") }
}
salida3 errores:
  - no se pudo abrir la base de datos
  - tiempo de espera agotado al conectar
  - disco al 95 %

El .toList() del final no es decoración. La secuencia solo existe mientras el archivo está abierto, es decir, dentro del bloque. Si la devuelves tal cual para usarla fuera, el archivo ya está cerrado cuando vas a leer:

import java.io.File

fun main() {
    val lineas = File("servidor.log").useLines { it }
    println(lineas.count())
}
salidaException in thread "main" java.io.IOException: Stream closed

Dentro de useLines, termina siempre con algo que consuma la secuencia: toList(), count(), first(), forEach… Lo mismo pasa con use en cualquier recurso.

Escribir mucho: bufferedWriter y use

Para escribir muchas líneas, appendText en un bucle abriría y cerraría el archivo cada vez. Mejor un BufferedWriter, que acumula en memoria y escribe en bloques. use lo cierra al salir del bloque aunque haya una excepción, igual que el try-con-recursos de Java:

import java.io.File

fun main() {
    File("tabla.txt").bufferedWriter().use { salida ->
        for (n in 1..10) {
            salida.write("7 x $n = ${7 * n}")
            salida.newLine()
        }
    }
    val lineas = File("tabla.txt").readLines()
    println("${lineas.size} líneas, de «${lineas.first()}» a «${lineas.last()}»")
}
salida10 líneas, de «7 x 1 = 7» a «7 x 10 = 70»

Si olvidas cerrar un BufferedWriter, lo último que escribiste puede quedarse en el búfer y no llegar nunca al disco. Con use no hay forma de olvidarlo.

Rutas y carpetas con kotlin.io.path

Para trabajar con carpetas, la API moderna es java.nio.file.Path, y el paquete kotlin.io.path le añade funciones con nombres claros y el operador / para unir rutas:

import kotlin.io.path.*

fun main() {
    val carpeta = Path("respaldos", "2026", "noviembre")
    carpeta.createDirectories()
    for (nombre in listOf("lunes.txt", "martes.txt", "notas.md")) {
        (carpeta / nombre).writeText("contenido de $nombre\n")
    }
    println("¿Existe? ${carpeta.exists()}  ¿Es carpeta? ${carpeta.isDirectory()}")

    for (archivo in carpeta.listDirectoryEntries("*.txt").sorted()) {
        println("${archivo.name}  extensión ${archivo.extension}  ${archivo.fileSize()} bytes")
    }

    val lunes = carpeta / "lunes.txt"
    println(lunes)
    println("${lunes.nameWithoutExtension} está en ${lunes.parent.name}")

    lunes.copyTo(carpeta / "lunes-copia.txt", overwrite = true)
    (carpeta / "martes.txt").deleteExisting()
    println(carpeta.listDirectoryEntries().map { it.name }.sorted())
}
salida¿Existe? true  ¿Es carpeta? true
lunes.txt  extensión txt  23 bytes
martes.txt  extensión txt  24 bytes
respaldos/2026/noviembre/lunes.txt
lunes está en noviembre
[lunes-copia.txt, lunes.txt, notas.md]
  • ParaCon Path (kotlin.io.path)
  • Crear una rutaPath("a", "b") o carpeta / "x.txt"
  • Crear carpetas (y las de en medio)createDirectories(), no falla si ya existen
  • Leer y escribirreadText(), writeText(), readLines(), useLines { }
  • Listar una carpetalistDirectoryEntries("*.txt")
  • Partes del nombrename, nameWithoutExtension, extension, parent
  • Copiar, mover, borrarcopyTo, moveTo, deleteExisting() o deleteIfExists()

listDirectoryEntries no garantiza ningún orden (depende del sistema de archivos): si lo vas a mostrar, ordénalo, como hace el ejemplo con sorted().

Cuando el archivo no existe

import java.io.File

fun main() {
    val texto = File("config.txt").readText()
    println(texto)
}
salidaException in thread "main" java.io.FileNotFoundException: config.txt (No such file or directory)

Kotlin no tiene excepciones comprobadas: el compilador no te obliga a atrapar IOException como haría Java, así que es fácil olvidarlo. Si el archivo puede faltar, compruébalo antes; y si puede fallar por otra causa (permisos, un disco desconectado), atrapa IOException, que es la madre de FileNotFoundException:

import java.io.File
import java.io.IOException

fun leerConfig(ruta: String): String {
    val archivo = File(ruta)
    if (!archivo.exists()) return "modo=normal (valor por omisión)"
    return try {
        archivo.readText().trim()
    } catch (e: IOException) {
        "no se pudo leer $ruta: ${e.message}"
    }
}

fun main() {
    println(leerConfig("config.txt"))
    File("config.txt").writeText("modo=oscuro\n")
    println(leerConfig("config.txt"))
}
salidamodo=normal (valor por omisión)
modo=oscuro

try en Kotlin es una expresión: devuelve el valor de su bloque o el del catch, y por eso puede ir detrás de return.

Si vienes de Java: las mismas clases, otros nombres

Todo esto son clases de Java (File, Path, BufferedWriter) con funciones de extensión de Kotlin encima. readText() equivale a Files.readString(path) y use al try-con-recursos. Compáralo con crear archivos de texto en Java y leer un archivo de texto en Java.

Todo el código de esta página se compiló con Kotlin 2.4.21 y se ejecutó en la JVM de Temurin 25.0.5 en un contenedor limpio y sin red antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.