Saltar al contenido
ficheros.java · devschool

Leer y escribir ficheros en Java

Lección 25 de 26 · 11 min de lectura · Actualizado el

En esta lección
  1. Path y Files: las herramientas modernas
  2. Excepciones de entrada y salida: IOException
  3. Leer ficheros de texto
  4. Escribir ficheros de texto
  5. Codificación: UTF-8
  6. Trabajar con carpetas
  7. Ejemplo completo: de CSV a objetos y a un informe
  8. Errores frecuentes
  9. Resumen

Todo lo que guarda un programa en variables desaparece al cerrarlo. Para que los datos sobrevivan (la lista de tareas, las notas de una clase, el registro de ventas de una tienda) hay que guardarlos en ficheros. En esta lección aprenderás a leer y escribir ficheros de texto con las clases modernas Path y Files, a trabajar con carpetas, a manejar los errores de entrada y salida y, como ejemplo completo, a leer un CSV, convertirlo en objetos y escribir un informe.

Path y Files: las herramientas modernas

Java tiene dos generaciones de clases para ficheros. Las antiguas (File, FileReader, FileWriter) siguen funcionando y las verás en muchos apuntes, pero desde Java 7 existe el paquete java.nio.file, más cómodo y con mejores mensajes de error:

  • Path representa una ruta a un fichero o carpeta. No abre nada: solo describe dónde está.
  • Files es una clase con métodos static que hacen el trabajo: leer, escribir, copiar, borrar, crear carpetas…
import java.nio.file.Files;
import java.nio.file.Path;

Path ruta = Path.of("datos", "tareas.txt");
System.out.println(ruta);               // datos/tareas.txt
System.out.println(ruta.getFileName()); // tareas.txt
System.out.println(ruta.getParent());   // datos
System.out.println(Files.exists(ruta)); // true

Path.of("datos", "tareas.txt") une las partes con el separador correcto del sistema: / en Linux y macOS, \ en Windows. Por eso es mejor que escribir la ruta a mano.

Rutas relativas y absolutas

  • Una ruta relativa (datos/tareas.txt) se busca a partir de la carpeta desde la que se ejecuta el programa. En un IDE suele ser la carpeta raíz del proyecto.
  • Una ruta absoluta empieza desde la raíz del disco: /home/ana/datos/tareas.txt o C:\Users\Ana\datos\tareas.txt.

Usa rutas relativas siempre que puedas: así el programa funciona en cualquier ordenador. Si no sabes dónde está buscando Java, imprime ruta.toAbsolutePath().

Excepciones de entrada y salida: IOException

Casi todos los métodos de Files pueden lanzar IOException: el fichero no existe, no tienes permisos, el disco está lleno… Es una excepción checked, así que el compilador te obliga a tratarla. Como viste en excepciones, tienes dos opciones: capturarla o declararla con throws.

try {
    List<String> lineas = Files.readAllLines(Path.of("no-existe.txt"));
} catch (NoSuchFileException e) {
    System.out.println("No existe el fichero: " + e.getMessage());
} catch (IOException e) {
    System.out.println("Error de lectura: " + e.getMessage());
}
// No existe el fichero: no-existe.txt

NoSuchFileException es una subclase de IOException, por eso va primero. En los ejemplos siguientes supondremos que el método tiene throws IOException para no repetir el try en cada uno.

Leer ficheros de texto

Partimos de un fichero datos/tareas.txt con este contenido:

Comprar pan
Estudiar Java
Ir al gimnasio

readAllLines: todas las líneas en una lista

List<String> tareas = Files.readAllLines(ruta);
System.out.println(tareas.size()); // 3
for (String t : tareas) {
    System.out.println("- " + t);
}
// - Comprar pan
// - Estudiar Java
// - Ir al gimnasio

Devuelve una lista con una línea por elemento, sin los saltos de línea. Es la forma más cómoda para ficheros pequeños y medianos.

readString: todo el contenido en un String

String contenido = Files.readString(ruta);
System.out.print(contenido);
// Comprar pan
// Estudiar Java
// Ir al gimnasio

Útil cuando quieres el texto completo, por ejemplo para buscar una palabra o contar caracteres.

lines: un stream de líneas

Para ficheros muy grandes (un registro de millones de líneas) no conviene cargar todo en memoria. Files.lines devuelve un stream que va leyendo poco a poco. Como mantiene el fichero abierto, se usa con try-with-resources:

try (Stream<String> lineas = Files.lines(ruta)) {
    long conJava = lineas.filter(l -> l.contains("Java")).count();
    System.out.println(conJava); // 1
}

BufferedReader: leer línea a línea

Es la forma clásica, y la que encontrarás en código antiguo. readLine() devuelve la siguiente línea o null cuando se acaba el fichero:

try (BufferedReader lector = Files.newBufferedReader(ruta)) {
    String linea;
    int numero = 1;
    while ((linea = lector.readLine()) != null) {
        System.out.println(numero + ": " + linea);
        numero++;
    }
}
// 1: Comprar pan
// 2: Estudiar Java
// 3: Ir al gimnasio

El buffer hace que se lean bloques grandes del disco de una vez en lugar de carácter a carácter, lo que es mucho más rápido. El try-with-resources garantiza que el fichero se cierra aunque haya un error.

Escribir ficheros de texto

writeString y write

Path notas = Path.of("datos", "notas.txt");
Files.writeString(notas, "Primera línea\n");

Files.write(notas, List.of("uno", "dos", "tres")); // una línea por elemento
System.out.println(Files.readAllLines(notas));     // [uno, dos, tres]
  • writeString escribe un texto tal cual (tú pones los \n).
  • write con una lista escribe cada elemento en una línea.
  • Si el fichero no existe, lo crean. Si existe, lo sobrescriben por completo.

Añadir al final: APPEND

Para no borrar lo que había (un registro de actividad, por ejemplo), pasa la opción StandardOpenOption.APPEND:

Files.writeString(notas, "Primera línea\n");
Files.writeString(notas, "Segunda línea\n", StandardOpenOption.APPEND);
System.out.print(Files.readString(notas));
// Primera línea
// Segunda línea

Cuidado: sin APPEND, cada escritura borra el contenido anterior. Es un error muy fácil de cometer y los datos perdidos no se recuperan.

BufferedWriter

Si vas a escribir muchas líneas poco a poco, por ejemplo dentro de un bucle, usa un BufferedWriter:

Path registro = Path.of("datos", "registro.txt");
try (BufferedWriter escritor = Files.newBufferedWriter(registro)) {
    escritor.write("Inicio de sesión");
    escritor.newLine();
    escritor.write("Cierre de sesión");
    escritor.newLine();
}

Es importante cerrarlo (el try-with-resources lo hace por ti): lo escrito se guarda en el buffer y solo se vuelca al disco al cerrar o al llamar a flush(). Si no se cierra, puedes perder las últimas líneas.

FileWriter y PrintWriter

En muchos apuntes verás la forma clásica con FileWriter. El segundo argumento true indica que se añade al final; envuelto en un PrintWriter puedes usar println y printf como en la consola:

try (FileWriter fw = new FileWriter("datos/registro.txt", StandardCharsets.UTF_8, true);
     PrintWriter pw = new PrintWriter(fw)) {
    pw.println("Nueva entrada");
    pw.printf("Total: %d tareas%n", 3);
}

Codificación: UTF-8

Un fichero de texto se guarda como bytes, y la codificación dice cómo convertir esos bytes en letras. Si se escribe con una y se lee con otra, las tildes y la ñ salen como ñ o como un rombo negro con una interrogación.

  • Los métodos de Files (readAllLines, readString, writeString, newBufferedReader…) usan UTF-8 por defecto.
  • Desde Java 18, FileReader y FileWriter también usan UTF-8 por defecto. En versiones anteriores dependían del sistema, por eso conviene indicarlo siempre: new FileWriter(ruta, StandardCharsets.UTF_8).

Si un fichero viene de otro programa con otra codificación (por ejemplo, un CSV antiguo de Excel en Windows), puedes indicarla: Files.readAllLines(ruta, Charset.forName("windows-1252")).

Trabajar con carpetas

Path carpeta = Path.of("informes", "2025", "junio");
System.out.println(Files.exists(carpeta));      // false
Files.createDirectories(carpeta);               // crea todas las que falten
System.out.println(Files.isDirectory(carpeta)); // true

createDirectories crea también las carpetas intermedias y no da error si ya existen. createDirectory (singular) solo crea una y lanza FileAlreadyExistsException si ya estaba.

Para listar el contenido de una carpeta se usa Files.list, que devuelve un stream y también va con try-with-resources:

try (Stream<Path> contenido = Files.list(Path.of("datos"))) {
    contenido.sorted().forEach(p -> System.out.println(p.getFileName()));
}
// alumnos.csv
// notas.txt
// registro.txt
// tareas.txt

Otras operaciones útiles:

MétodoQué hace
Files.exists(ruta)Comprueba si existe
Files.isDirectory(ruta)Comprueba si es una carpeta
Files.size(ruta)Tamaño en bytes
Files.copy(origen, destino)Copia un fichero
Files.move(origen, destino)Mueve o renombra
Files.delete(ruta)Borra (error si no existe)
Files.deleteIfExists(ruta)Borra si existe

Ejemplo completo: de CSV a objetos y a un informe

Un CSV es un fichero de texto con datos en forma de tabla: una fila por línea y los campos separados por un carácter, normalmente , o ; (en España se usa mucho ; porque la coma es el separador decimal). Es el formato que exportan Excel, Google Sheets y muchas bases de datos.

Tenemos datos/alumnos.csv:

nombre;grupo;nota
Lucía;1B;8.5
Mario;1A;6.0
Aitana;1B;9.2
Hugo;1A;4.5

Leer el CSV y crear objetos

Cada línea se convierte en un objeto Alumno (un record). Los pasos son: leer las líneas, saltar la cabecera, partir cada línea con split y convertir cada campo a su tipo:

record Alumno(String nombre, String grupo, double nota) {}

static List<Alumno> leerAlumnos(Path ruta) throws IOException {
    List<Alumno> alumnos = new ArrayList<>();
    List<String> lineas = Files.readAllLines(ruta);
    for (int i = 1; i < lineas.size(); i++) {     // empieza en 1: salta la cabecera
        String linea = lineas.get(i).trim();
        if (linea.isEmpty()) {
            continue;                              // ignora líneas en blanco
        }
        String[] campos = linea.split(";");
        alumnos.add(new Alumno(campos[0], campos[1], Double.parseDouble(campos[2])));
    }
    return alumnos;
}
List<Alumno> alumnos = leerAlumnos(Path.of("datos", "alumnos.csv"));
alumnos.forEach(System.out::println);
// Alumno[nombre=Lucía, grupo=1B, nota=8.5]
// Alumno[nombre=Mario, grupo=1A, nota=6.0]
// Alumno[nombre=Aitana, grupo=1B, nota=9.2]
// Alumno[nombre=Hugo, grupo=1A, nota=4.5]

Si una nota viniera como 8,5 (con coma), Double.parseDouble lanzaría NumberFormatException. Podrías sustituir la coma antes: campos[2].replace(',', '.').

Escribir un informe

Ahora generamos un informe de texto con la nota de cada alumno y un resumen:

List<String> informe = new ArrayList<>();
informe.add("INFORME DE NOTAS");
informe.add("================");

double suma = 0;
int aprobados = 0;
for (Alumno a : alumnos) {
    String estado = a.nota() >= 5 ? "Aprobado" : "Suspenso";
    informe.add(a.nombre() + " (" + a.grupo() + "): " + a.nota() + " - " + estado);
    suma += a.nota();
    if (a.nota() >= 5) {
        aprobados++;
    }
}
informe.add("");
informe.add("Media: " + suma / alumnos.size());
informe.add("Aprobados: " + aprobados + " de " + alumnos.size());

Files.createDirectories(Path.of("informes"));
Files.write(Path.of("informes", "notas.txt"), informe);

Contenido de informes/notas.txt:

INFORME DE NOTAS
================
Lucía (1B): 8.5 - Aprobado
Mario (1A): 6.0 - Aprobado
Aitana (1B): 9.2 - Aprobado
Hugo (1A): 4.5 - Suspenso

Media: 7.05
Aprobados: 3 de 4

Este patrón (leer datos de un fichero, procesarlos y escribir un resultado) es la base de muchísimos programas reales. Cuando los datos crecen o varias personas deben usarlos a la vez, el siguiente paso es una base de datos.

Consejo: para CSV reales con comillas, campos que contienen el separador o saltos de línea dentro de un campo, un simple split no basta. En proyectos profesionales se usan bibliotecas como OpenCSV o Apache Commons CSV.

Errores frecuentes

  • No tratar IOException. El código no compila: captúrala o añade throws IOException.
  • Olvidar APPEND y sobrescribir el fichero en cada escritura.
  • No cerrar los recursos. Usa siempre try-with-resources con BufferedReader, BufferedWriter, Files.lines y Files.list.
  • Rutas mal escritas o relativas a una carpeta distinta de la que crees. Imprime ruta.toAbsolutePath() para comprobarlo.
  • Escribir rutas con \ a mano ("datos\tareas.txt"): \t es un tabulador. Usa Path.of("datos", "tareas.txt").
  • Mezclar codificaciones y obtener tildes rotas. Trabaja siempre en UTF-8.
  • Cargar con readAllLines un fichero enorme. Para eso está Files.lines.
  • Olvidar la cabecera del CSV y obtener NumberFormatException al convertir "nota" en número.

Resumen

TareaCódigo
Crear una rutaPath.of("datos", "tareas.txt")
Comprobar si existeFiles.exists(ruta)
Leer todas las líneasFiles.readAllLines(ruta)
Leer todo el textoFiles.readString(ruta)
Leer como streamtry (Stream<String> s = Files.lines(ruta)) { ... }
Leer línea a líneaFiles.newBufferedReader(ruta) + readLine()
Escribir (sobrescribe)Files.writeString(ruta, texto), Files.write(ruta, lista)
Añadir al finalFiles.writeString(ruta, texto, StandardOpenOption.APPEND)
Escribir muchoFiles.newBufferedWriter(ruta) + write / newLine
Crear carpetasFiles.createDirectories(ruta)
Listar carpetatry (Stream<Path> s = Files.list(carpeta)) { ... }
Error de E/SIOException (checked)

En la siguiente lección darás ese paso: conectar Java con una base de datos usando JDBC.

Pon a prueba lo que has aprendido

[Java] ¿Qué imprime este código?
Path ruta = Path.of("notas.txt");
Files.writeString(ruta, "Lunes\n");
Files.writeString(ruta, "Martes\n");
Files.writeString(ruta, "Jueves\n", StandardOpenOption.APPEND);
System.out.println(Files.readAllLines(ruta));

[Java] Este main no compila. ¿Por qué?
public static void main(String[] args) {
    List<String> lineas = Files.readAllLines(Path.of("tareas.txt"));
    System.out.println(lineas.size());
}

[Java] Un programa en Windows intenta leer el fichero "datos\tareas.txt" y dice que no existe, aunque está ahí. ¿Cuál es la causa más probable?
Path ruta = Path.of("datos\tareas.txt");

[Java] Al leer este CSV con el código siguiente, ¿qué ocurre?
// alumnos.csv:
// nombre;nota
// Lucía;8.5
List<String> lineas = Files.readAllLines(Path.of("alumnos.csv"));
for (String linea : lineas) {
    String[] campos = linea.split(";");
    System.out.println(Double.parseDouble(campos[1]));
}

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.