Módulo 17: Ficheros
Objetivos del módulo
- Entender qué es la persistencia y distinguir ficheros de texto y binarios
- Trabajar con rutas absolutas y relativas mediante
PathyPath.of - Usar la clase
Filespara crear, leer, escribir, copiar, mover, listar y borrar - Leer y escribir texto con
BufferedReaderyBufferedWriteren UTF-8, y añadir al final de un fichero - Leer y escribir ficheros CSV, y cargar objetos desde ellos
- Leer ficheros con
Scanner - Escribir y leer ficheros binarios con
DataOutputStreamyDataInputStream - Serializar objetos (
Serializable,transient,serialVersionUID) y conocer sus riesgos - Gestionar los errores de entrada/salida (
IOException,NoSuchFileException…) - Conocer la clase heredada
File
1. Persistencia: por qué guardar datos en ficheros
Todo lo que has programado hasta ahora vive en la memoria RAM: variables, arrays, objetos, colecciones. Cuando el programa termina (o se va la luz), todo se pierde. Una agenda que olvida los contactos al cerrarse no sirve de mucho.
📘 Concepto: La persistencia es la capacidad de un programa de conservar datos entre ejecuciones, guardándolos en un almacenamiento permanente: un fichero en disco o una base de datos (Curso Avanzado). Un fichero es una secuencia de bytes con un nombre, guardada dentro de una carpeta (directorio).
1.1. Ficheros de texto y ficheros binarios
| Fichero de texto | Fichero binario | |
|---|---|---|
| Contenido | Caracteres legibles, organizados en líneas | Bytes con cualquier valor: números en formato interno, imágenes, sonido… |
| ¿Se puede abrir con un editor? | Sí (Bloc de notas, VS Code) | No: verías símbolos sin sentido |
| Ejemplos | .txt, .csv, .json, .html, .java, .log | .png, .mp3, .pdf, .class, .dat |
| Ventajas | Legible, portable, fácil de depurar y de editar | Compacto y rápido; guarda los tipos exactamente |
| Clases de Java | Reader/Writer, Files.readString… | InputStream/OutputStream, DataInputStream… |
En un fichero de texto, cada carácter se guarda como uno o varios bytes según una codificación (charset). La más usada hoy es UTF-8, que representa todos los caracteres del mundo (ñ, á, €, emojis…). Desde Java 18, UTF-8 es la codificación por defecto en todas las plataformas, y las clases de java.nio.file.Files siempre la han usado por defecto.
⚠️ Importante: Si un fichero se escribe con una codificación y se lee con otra, las tildes y las eñes se convierten en caracteres raros (
canción). Usa UTF-8 siempre y, cuando una clase permita indicarlo, indícalo explícitamente (StandardCharsets.UTF_8).
2. Rutas: Path
2.1. Rutas absolutas y relativas
Para trabajar con un fichero hay que indicar dónde está mediante una ruta:
Figura 1. Rutas absolutas y relativas.
- Una ruta absoluta empieza en la raíz del sistema de ficheros:
/home/alumno/proyecto/datos/clientes.csv(Linux y macOS) oC:\Users\alumno\proyecto\datos\clientes.csv(Windows). - Una ruta relativa se interpreta a partir del directorio de trabajo del programa:
datos/clientes.csv. Cuando ejecutas desde IntelliJ IDEA, el directorio de trabajo es, por defecto, la carpeta raíz del proyecto (no la carpetasrc); desde la terminal, la carpeta en la que estás.
💡 Consejo: Usa rutas relativas en tus programas (por ejemplo, una carpeta
datosdentro del proyecto). Una ruta absoluta comoC:\Users\Ana\Escritorio\datos.txtsolo funciona en el ordenador de Ana.
2.2. La interfaz Path
Desde Java 7, las rutas se representan con la interfaz java.nio.file.Path. Se crean con Path.of(...) (Java 11), que une las partes con el separador correcto del sistema (/ o \). En código antiguo verás Paths.get(...), que hace exactamente lo mismo.
import java.nio.file.Path;
public class Rutas {
public static void main(String[] args) {
System.out.println("Directorio de trabajo: " + System.getProperty("user.dir"));
Path relativa = Path.of("datos", "clientes.csv"); // une con el separador del sistema
System.out.println("Ruta relativa: " + relativa);
System.out.println(" ¿Absoluta? " + relativa.isAbsolute());
System.out.println(" Absoluta: " + relativa.toAbsolutePath());
System.out.println(" Nombre del fichero: " + relativa.getFileName());
System.out.println(" Carpeta padre: " + relativa.getParent());
System.out.println(" Número de partes: " + relativa.getNameCount());
Path base = Path.of("informes");
System.out.println("resolve: " + base.resolve("2026").resolve("enero.txt"));
System.out.println("resolveSibling: " + relativa.resolveSibling("proveedores.csv"));
Path rara = Path.of("datos/../informes/./enero.txt");
System.out.println("Sin normalizar: " + rara);
System.out.println("Normalizada: " + rara.normalize());
Path desde = Path.of("informes", "2026");
Path hasta = Path.of("datos", "clientes.csv");
System.out.println("Cómo ir de " + desde + " a " + hasta + ": " + desde.relativize(hasta));
}
}
Salida (ejecutado en Linux, en la carpeta /home/alumno/proyecto):
Directorio de trabajo: /home/alumno/proyecto
Ruta relativa: datos/clientes.csv
¿Absoluta? false
Absoluta: /home/alumno/proyecto/datos/clientes.csv
Nombre del fichero: clientes.csv
Carpeta padre: datos
Número de partes: 2
resolve: informes/2026/enero.txt
resolveSibling: datos/proveedores.csv
Sin normalizar: datos/../informes/./enero.txt
Normalizada: informes/enero.txt
Cómo ir de informes/2026 a datos/clientes.csv: ../../datos/clientes.csv
En Windows verías las mismas rutas con \ (datos\clientes.csv, C:\Users\alumno\proyecto\datos\clientes.csv). Crear un Path no crea ni comprueba nada en el disco: es solo una dirección.
| Método | Qué devuelve |
|---|---|
Path.of("a", "b", "c.txt") | La ruta a/b/c.txt |
getFileName() / getParent() | El último elemento / la ruta sin el último elemento |
toAbsolutePath() / isAbsolute() | La ruta absoluta / si ya lo es |
resolve("x") | La ruta con x añadido al final (entrar en una subcarpeta o fichero) |
resolveSibling("x") | Un “hermano”: misma carpeta, otro nombre |
normalize() | Elimina . y .. innecesarios |
relativize(otra) | La ruta relativa para ir de esta a otra |
3. La clase Files
La clase java.nio.file.Files reúne métodos estáticos para casi todo lo que se hace con ficheros y carpetas. Casi todos lanzan IOException, una excepción comprobada (módulo 15), así que hay que capturarla o declararla con throws. En los primeros ejemplos la declaramos en main para centrarnos en los ficheros; en la sección 4 verás cómo tratarla bien.
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.stream.Stream;
public class FilesBasico {
public static void main(String[] args) throws IOException {
Path carpeta = Path.of("datos");
Files.createDirectories(carpeta); // no falla si ya existe
Path saludo = carpeta.resolve("saludo.txt");
// Escribir y leer un fichero completo de una vez
Files.writeString(saludo, "Hola, mundo\nSegunda línea con ñ y tildes: canción\n");
String contenido = Files.readString(saludo); // UTF-8 por defecto
System.out.print("readString:\n" + contenido);
// Trabajar línea a línea
List<String> lineas = Files.readAllLines(saludo);
System.out.println("readAllLines: " + lineas.size() + " líneas -> " + lineas);
Files.write(carpeta.resolve("compra.txt"), List.of("pan", "leche", "huevos"));
// Información
System.out.println("¿Existe? " + Files.exists(saludo)
+ ", ¿es fichero? " + Files.isRegularFile(saludo)
+ ", ¿es carpeta? " + Files.isDirectory(carpeta));
System.out.println("Tamaño: " + Files.size(saludo) + " bytes");
// Copiar, mover y borrar
Files.copy(saludo, carpeta.resolve("copia.txt"), StandardCopyOption.REPLACE_EXISTING);
Files.move(carpeta.resolve("copia.txt"), carpeta.resolve("renombrado.txt"),
StandardCopyOption.REPLACE_EXISTING);
// Listar el contenido de una carpeta
System.out.println("Contenido de " + carpeta + ":");
try (DirectoryStream<Path> dir = Files.newDirectoryStream(carpeta)) {
for (Path p : dir) {
System.out.println(" " + p.getFileName() + " (" + Files.size(p) + " bytes)");
}
}
// Files.lines y Files.list devuelven un Stream (lectura "bajo demanda"): hay que cerrarlos
try (Stream<String> lineasStream = Files.lines(saludo);
Stream<Path> entradas = Files.list(carpeta)) {
System.out.println("Files.lines: " + lineasStream.count() + " líneas; Files.list: "
+ entradas.count() + " entradas");
}
Files.delete(carpeta.resolve("renombrado.txt"));
System.out.println("¿Borrado? " + !Files.exists(carpeta.resolve("renombrado.txt")));
System.out.println("deleteIfExists de algo que no existe: "
+ Files.deleteIfExists(carpeta.resolve("no-existe.txt")));
}
}
Salida:
readString:
Hola, mundo
Segunda línea con ñ y tildes: canción
readAllLines: 2 líneas -> [Hola, mundo, Segunda línea con ñ y tildes: canción]
¿Existe? true, ¿es fichero? true, ¿es carpeta? true
Tamaño: 53 bytes
Contenido de datos:
compra.txt (17 bytes)
renombrado.txt (53 bytes)
saludo.txt (53 bytes)
Files.lines: 2 líneas; Files.list: 3 entradas
¿Borrado? true
deleteIfExists de algo que no existe: false
| Método | Qué hace |
|---|---|
exists(p), isRegularFile(p), isDirectory(p), size(p) | Consultas sobre la ruta |
createDirectory(p) / createDirectories(p) | Crea una carpeta / la carpeta y todas las intermedias (sin error si ya existe) |
writeString(p, texto) / readString(p) | Escribe / lee un fichero de texto completo (Java 11) |
write(p, lineas) / readAllLines(p) | Escribe / lee una lista de líneas |
lines(p) | Lee las líneas bajo demanda como un Stream (para ficheros enormes; hay que cerrarlo) |
newBufferedReader(p) / newBufferedWriter(p) | Abre un lector / escritor de texto con buffer (sección 5) |
copy(origen, destino, opciones) / move(...) | Copia / mueve o renombra (con REPLACE_EXISTING sobrescribe) |
delete(p) / deleteIfExists(p) | Borra (la primera lanza excepción si no existe) |
newDirectoryStream(carpeta) / list(carpeta) | Recorre el contenido de una carpeta |
⚠️ Importante:
readStringyreadAllLinescargan el fichero entero en memoria. Son perfectos para ficheros pequeños y medianos (configuración, listas, CSV de unos miles de líneas). Para ficheros muy grandes (logs de gigas), lee línea a línea conBufferedReaderoFiles.lines.
4. Gestión de errores con ficheros
Trabajar con ficheros es trabajar con algo externo al programa: el fichero puede no existir, estar abierto por otro programa, no tener permisos o estar en un disco lleno. Por eso Java obliga a tratar IOException y sus subclases:
Excepción (todas heredan de IOException) | Cuándo ocurre |
|---|---|
NoSuchFileException | El fichero o la carpeta no existen |
FileAlreadyExistsException | Creas algo que ya existe (createDirectory, copy sin REPLACE_EXISTING) |
DirectoryNotEmptyException | Borras una carpeta que no está vacía |
AccessDeniedException | No tienes permisos |
EOFException | Intentas leer datos binarios más allá del final del fichero |
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
public class ErroresFicheros {
public static void main(String[] args) throws IOException {
try {
String texto = Files.readString(Path.of("no-existe.txt"));
System.out.println(texto);
} catch (NoSuchFileException e) {
System.out.println("No existe el fichero: " + e.getMessage());
} catch (IOException e) {
System.out.println("Error de lectura: " + e.getMessage());
}
Files.createDirectories(Path.of("carpeta"));
Files.writeString(Path.of("carpeta", "a.txt"), "algo");
try {
Files.createDirectory(Path.of("carpeta")); // ya existe
} catch (FileAlreadyExistsException e) {
System.out.println("Ya existe: " + e.getMessage());
}
try {
Files.delete(Path.of("carpeta")); // no está vacía
} catch (DirectoryNotEmptyException e) {
System.out.println("La carpeta no está vacía: " + e.getMessage());
}
// Sin capturar: así se ve la traza
Files.readAllLines(Path.of("datos", "falta.csv"));
}
}
Salida:
No existe el fichero: no-existe.txt
Ya existe: carpeta
La carpeta no está vacía: carpeta
Exception in thread "main" java.nio.file.NoSuchFileException: datos/falta.csv
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixFileSystemProvider.newFileChannel(UnixFileSystemProvider.java:213)
at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:244)
at java.base/java.nio.file.Files.newByteChannel(Files.java:357)
at java.base/java.nio.file.Files.newByteChannel(Files.java:399)
at java.base/java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:371)
at java.base/java.nio.file.Files.newInputStream(Files.java:154)
at java.base/java.nio.file.Files.newBufferedReader(Files.java:2645)
at java.base/java.nio.file.Files.readAllLines(Files.java:3085)
at java.base/java.nio.file.Files.readAllLines(Files.java:3122)
at ErroresFicheros.main(ErroresFicheros.java:33)
La traza de la última operación es larga porque readAllLines llama a muchos métodos internos (sun.nio.fs...), pero se lee como en el módulo 15: el tipo (NoSuchFileException), el mensaje (la ruta que no existe) y la primera línea de tu código (ErroresFicheros.main(ErroresFicheros.java:33)). En Windows el tipo sería el mismo, aunque las líneas internas cambien (sun.nio.fs.WindowsException...).
💡 Consejo: Captura
NoSuchFileExceptioncuando quieras dar un mensaje específico (“no encuentro el fichero”) yIOExceptionpara el resto. Recuerda el orden del módulo 15: la más específica primero.
5. Leer y escribir texto con BufferedReader y BufferedWriter
5.1. Flujos (streams) de entrada y salida
Por debajo, Java lee y escribe ficheros mediante flujos (streams): canales por los que los datos circulan en orden, de uno en uno o en bloques. Hay flujos de bytes (InputStream/OutputStream) y de caracteres (Reader/Writer), y se combinan por capas: cada una envuelve a la anterior y le añade una capacidad.
Figura 2. Los flujos de entrada y salida se apilan en capas.
El buffer es la capa más importante para el rendimiento: en lugar de ir al disco por cada carácter, se leen o escriben bloques de varios miles de bytes de una vez.
⚠️ Importante: No confundas estos streams de entrada/salida (paquete
java.io) con los streams de datos dejava.util.stream(como el que devuelveFiles.lines), que se estudian en el Curso Avanzado. Comparten nombre, pero son conceptos distintos.
5.2. Ejemplo completo
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class TextoBuffered {
public static void main(String[] args) {
Path fichero = Path.of("poema.txt");
// Escribir con BufferedWriter (UTF-8 por defecto)
try (BufferedWriter out = Files.newBufferedWriter(fichero)) {
out.write("Caminante, son tus huellas");
out.newLine(); // salto de línea del sistema
out.write("el camino y nada más;");
out.newLine();
out.write("caminante, no hay camino,");
out.newLine();
out.write("se hace camino al andar.");
out.newLine();
} catch (IOException e) {
System.out.println("No se pudo escribir: " + e.getMessage());
return;
}
// Leer línea a línea con BufferedReader
try (BufferedReader in = Files.newBufferedReader(fichero)) {
String linea;
int numero = 1;
while ((linea = in.readLine()) != null) { // null = fin del fichero
System.out.printf("%2d: %s%n", numero++, linea);
}
} catch (IOException e) {
System.out.println("No se pudo leer: " + e.getMessage());
}
// La forma "clásica" con FileWriter/FileReader (indicando siempre la codificación)
try (BufferedWriter out = new BufferedWriter(new FileWriter("clasico.txt", StandardCharsets.UTF_8));
BufferedReader in = new BufferedReader(new FileReader("poema.txt", StandardCharsets.UTF_8))) {
String linea;
while ((linea = in.readLine()) != null) {
out.write(linea.toUpperCase());
out.newLine();
}
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
try {
System.out.println("Primera línea de clasico.txt: " + Files.readAllLines(Path.of("clasico.txt")).get(0));
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Salida:
1: Caminante, son tus huellas
2: el camino y nada más;
3: caminante, no hay camino,
4: se hace camino al andar.
Primera línea de clasico.txt: CAMINANTE, SON TUS HUELLAS
Claves del ejemplo:
- Siempre con try-with-resources (módulo 15): el fichero se cierra solo, haya error o no. Un
BufferedWriterque no se cierra puede no llegar a escribir lo que tenía en su buffer. readLine()devuelve la siguiente línea sin el salto de línea, onullal llegar al final. El buclewhile ((linea = in.readLine()) != null)es el patrón clásico.newLine()escribe el salto de línea del sistema (\nen Linux y macOS,\r\nen Windows).Files.newBufferedReader/Writerusan UTF-8. Si usasFileReader/FileWriter, indica la codificación con el constructor que recibe unCharset(Java 11).
5.3. Añadir al final de un fichero: APPEND
Por defecto, escribir en un fichero borra lo que tenía. Para añadir al final, usa las opciones StandardOpenOption.CREATE (créalo si no existe) y StandardOpenOption.APPEND:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class Append {
public static void main(String[] args) throws IOException {
Path fichero = Path.of("visitas.txt");
Files.writeString(fichero, "visita 1\n"); // crea o SOBRESCRIBE
Files.writeString(fichero, "visita 2\n"); // ¡ha borrado la 1!
System.out.print("Sin APPEND:\n" + Files.readString(fichero));
Files.writeString(fichero, "visita 3\n",
StandardOpenOption.CREATE, StandardOpenOption.APPEND); // añade al final
Files.writeString(fichero, "visita 4\n",
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
System.out.print("Con APPEND:\n" + Files.readString(fichero));
}
}
Salida:
Sin APPEND:
visita 2
Con APPEND:
visita 2
visita 3
visita 4
Las mismas opciones sirven en Files.newBufferedWriter(ruta, CREATE, APPEND) y en Files.write. Con las clases clásicas, el equivalente es new FileWriter("fichero.txt", StandardCharsets.UTF_8, true): el último parámetro true significa “añadir”.
5.4. Ejemplo: un registro de log
Un log es un fichero donde un programa va anotando lo que hace (inicio, avisos, errores) para poder revisarlo después. Es el uso más típico de APPEND:
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
class Log {
enum Nivel { INFO, AVISO, ERROR }
private static final DateTimeFormatter FORMATO = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final Path fichero;
Log(Path fichero) {
this.fichero = fichero;
}
void escribir(Nivel nivel, String mensaje) {
String linea = LocalDateTime.now().format(FORMATO) + " [" + nivel + "] " + mensaje;
try (BufferedWriter out = Files.newBufferedWriter(fichero,
StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
out.write(linea);
out.newLine();
} catch (IOException e) {
// Si ni siquiera podemos escribir el log, avisamos por la salida de error
System.err.println("No se pudo escribir en el log: " + e.getMessage());
}
}
List<String> leer(Nivel nivel) throws IOException {
List<String> resultado = new ArrayList<>();
for (String linea : Files.readAllLines(fichero)) {
if (linea.contains("[" + nivel + "]")) {
resultado.add(linea);
}
}
return resultado;
}
}
public class RegistroLog {
public static void main(String[] args) throws IOException {
Log log = new Log(Path.of("aplicacion.log"));
log.escribir(Log.Nivel.INFO, "Aplicación iniciada");
log.escribir(Log.Nivel.AVISO, "Fichero de configuración no encontrado: se usan valores por defecto");
try {
Integer.parseInt("abc");
} catch (NumberFormatException e) {
log.escribir(Log.Nivel.ERROR, "Dato no válido: " + e.getMessage());
}
log.escribir(Log.Nivel.INFO, "Aplicación finalizada");
System.out.print(Files.readString(Path.of("aplicacion.log")));
System.out.println("Solo errores: " + log.leer(Log.Nivel.ERROR).size());
}
}
Salida (las horas son las de esa ejecución):
2026-10-04 12:27:03 [INFO] Aplicación iniciada
2026-10-04 12:27:04 [AVISO] Fichero de configuración no encontrado: se usan valores por defecto
2026-10-04 12:27:04 [ERROR] Dato no válido: For input string: "abc"
2026-10-04 12:27:04 [INFO] Aplicación finalizada
Solo errores: 1
Cada llamada a escribir abre el fichero en modo APPEND, añade una línea y lo cierra, así que el log sobrevive entre ejecuciones: si vuelves a ejecutar el programa, las nuevas líneas se añaden debajo. En aplicaciones profesionales se usan bibliotecas de log (java.util.logging, SLF4J con Logback), que verás en el Curso Avanzado; la idea es la misma.
6. Ficheros CSV
6.1. El formato CSV
Un fichero CSV (Comma-Separated Values) guarda una tabla en texto: una línea por fila y los campos separados por un carácter. Lo abren Excel, LibreOffice Calc y casi cualquier programa de datos.
nombre;edad;nota
Lucía Pérez;19;8.5
Mario Gil;20;6.25
- La primera línea suele ser una cabecera con los nombres de las columnas.
- En España se usa a menudo
;como separador, porque la coma es el separador decimal. Ojo:Double.parseDoublesolo entiende el punto (8.5), no la coma. - Para leer una línea, se divide con
split(";"); para escribirla, se unen los campos conString.join(";", ...)o concatenando.
6.2. Guardar y cargar objetos en CSV
La técnica habitual es que la propia clase sepa convertirse en una línea (aCsv()) y crearse a partir de una (desdeCsv()):
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
record Alumno(String nombre, int edad, double nota) {
// Convierte el objeto en una línea CSV
String aCsv() {
return nombre + ";" + edad + ";" + nota;
}
// Crea un objeto a partir de una línea CSV (o lanza una excepción si está mal)
static Alumno desdeCsv(String linea) {
String[] campos = linea.split(";");
if (campos.length != 3) {
throw new IllegalArgumentException("se esperaban 3 campos y hay " + campos.length);
}
return new Alumno(campos[0].strip(), Integer.parseInt(campos[1].strip()),
Double.parseDouble(campos[2].strip()));
}
}
public class CsvAlumnos {
static void guardar(List<Alumno> alumnos, Path fichero) throws IOException {
try (BufferedWriter out = Files.newBufferedWriter(fichero)) {
out.write("nombre;edad;nota"); // cabecera
out.newLine();
for (Alumno a : alumnos) {
out.write(a.aCsv());
out.newLine();
}
}
}
static List<Alumno> cargar(Path fichero) throws IOException {
List<Alumno> alumnos = new ArrayList<>();
try (BufferedReader in = Files.newBufferedReader(fichero)) {
String linea = in.readLine(); // saltamos la cabecera
int numLinea = 1;
while ((linea = in.readLine()) != null) {
numLinea++;
if (linea.isBlank()) {
continue;
}
try {
alumnos.add(Alumno.desdeCsv(linea));
} catch (IllegalArgumentException e) { // incluye NumberFormatException
System.out.println(" Línea " + numLinea + " ignorada (" + e.getMessage() + "): " + linea);
}
}
}
return alumnos;
}
public static void main(String[] args) throws IOException {
Path fichero = Path.of("alumnos.csv");
guardar(List.of(new Alumno("Lucía Pérez", 19, 8.5), new Alumno("Mario Gil", 20, 6.25),
new Alumno("Sofía Ríos", 18, 9.0)), fichero);
// Simulamos que alguien edita el fichero a mano y comete errores
Files.writeString(fichero, "Pablo Sanz;veinte;7\nIrene Mora;21\n\nHugo León; 22 ; 4.75\n",
java.nio.file.StandardOpenOption.APPEND);
System.out.print("Contenido de " + fichero + ":\n" + Files.readString(fichero));
System.out.println("Cargando...");
List<Alumno> alumnos = cargar(fichero);
double suma = 0;
for (Alumno a : alumnos) {
System.out.println(" " + a);
suma += a.nota();
}
System.out.printf("%d alumnos cargados, nota media %.2f%n", alumnos.size(), suma / alumnos.size());
}
}
Salida:
Contenido de alumnos.csv:
nombre;edad;nota
Lucía Pérez;19;8.5
Mario Gil;20;6.25
Sofía Ríos;18;9.0
Pablo Sanz;veinte;7
Irene Mora;21
Hugo León; 22 ; 4.75
Cargando...
Línea 5 ignorada (For input string: "veinte"): Pablo Sanz;veinte;7
Línea 6 ignorada (se esperaban 3 campos y hay 2): Irene Mora;21
Alumno[nombre=Lucía Pérez, edad=19, nota=8.5]
Alumno[nombre=Mario Gil, edad=20, nota=6.25]
Alumno[nombre=Sofía Ríos, edad=18, nota=9.0]
Alumno[nombre=Hugo León, edad=22, nota=4.75]
4 alumnos cargados, nota media 7.13
Observa cómo se trata cada problema línea a línea: una línea errónea se avisa y se salta, pero no impide cargar las demás (try/catch dentro del bucle, como en el ejercicio 2 del módulo 15). NumberFormatException hereda de IllegalArgumentException, así que un solo catch recoge los dos tipos de error. Las líneas en blanco se ignoran y strip() elimina los espacios sobrantes (" 22 ").
⚠️ Importante:
splittiene dos trampas. Por defecto elimina los campos vacíos del final:"Ana;611333444;".split(";")da 2 campos, no 3; usasplit(";", -1)para conservarlos. Y no sabe nada de comillas: si un campo puede contener el separador ("García; Ana"), necesitas un formato con comillas y una biblioteca como OpenCSV o Apache Commons CSV.
6.3. Ejemplo: agenda que persiste en CSV
Ahora sí, una agenda que recuerda sus contactos. Al crearla, carga el fichero si existe; guardar() lo reescribe con el contenido actual. Para simular dos ejecuciones del programa, el main crea dos agendas sobre el mismo fichero, una después de otra:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
record Contacto(String nombre, String telefono, String email) {
String aCsv() {
return String.join(";", nombre, telefono, email);
}
static Contacto desdeCsv(String linea) {
String[] c = linea.split(";", -1); // -1: conserva campos vacíos al final
if (c.length != 3) {
throw new IllegalArgumentException("línea mal formada: " + linea);
}
return new Contacto(c[0], c[1], c[2]);
}
}
class Agenda {
private final Path fichero;
private final Map<String, Contacto> contactos = new TreeMap<>();
Agenda(Path fichero) throws IOException {
this.fichero = fichero;
cargar();
}
private void cargar() throws IOException {
if (!Files.exists(fichero)) {
System.out.println("(No hay agenda guardada: se empieza vacía)");
return;
}
for (String linea : Files.readAllLines(fichero)) {
if (!linea.isBlank()) {
Contacto c = Contacto.desdeCsv(linea);
contactos.put(c.nombre().toLowerCase(), c);
}
}
System.out.println("(Cargados " + contactos.size() + " contactos de " + fichero + ")");
}
void guardar() throws IOException {
List<String> lineas = new ArrayList<>();
for (Contacto c : contactos.values()) {
lineas.add(c.aCsv());
}
Path temporal = fichero.resolveSibling(fichero.getFileName() + ".tmp");
Files.write(temporal, lineas); // primero a un temporal...
Files.move(temporal, fichero, java.nio.file.StandardCopyOption.REPLACE_EXISTING); // ...y luego se sustituye
}
void anadir(Contacto c) {
contactos.put(c.nombre().toLowerCase(), c);
}
boolean borrar(String nombre) {
return contactos.remove(nombre.toLowerCase()) != null;
}
int tamano() {
return contactos.size();
}
}
public class AgendaCsv {
public static void main(String[] args) throws IOException {
Path fichero = Path.of("agenda.csv");
System.out.println("== Primera ejecución ==");
Agenda agenda = new Agenda(fichero);
agenda.anadir(new Contacto("Pedro Gil", "600111222", "pedro@ejemplo.es"));
agenda.anadir(new Contacto("Ana Ruiz", "611333444", ""));
agenda.guardar();
System.out.println("== Segunda ejecución (otro día) ==");
Agenda otra = new Agenda(fichero); // lee lo guardado
otra.anadir(new Contacto("Eva Martín", "622555666", "eva@ejemplo.es"));
otra.borrar("pedro gil");
otra.guardar();
System.out.println("Contactos: " + otra.tamano());
System.out.print("Fichero " + fichero + ":\n" + Files.readString(fichero));
}
}
Salida:
== Primera ejecución ==
(No hay agenda guardada: se empieza vacía)
== Segunda ejecución (otro día) ==
(Cargados 2 contactos de agenda.csv)
Contactos: 2
Fichero agenda.csv:
Ana Ruiz;611333444;
Eva Martín;622555666;eva@ejemplo.es
Figura 3. El ciclo de guardar y cargar objetos en un fichero.
Dos detalles profesionales:
split(";", -1)conserva el email vacío de Ana.guardar()escribe primero en un fichero temporal y después lo mueve sobre el definitivo. Si el programa fallara a mitad de la escritura, el fichero original seguiría intacto: nunca te quedas con una agenda medio escrita.
7. Leer ficheros con Scanner
El Scanner del módulo 04 también puede leer de un fichero: basta con pasarle un Path en lugar de System.in. Es cómodo para leer números o palabras separados por espacios:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Scanner;
public class ScannerFichero {
public static void main(String[] args) throws IOException {
Path fichero = Path.of("medidas.txt");
Files.writeString(fichero, "12 7 19\n4 x 30\n\n8\n");
int suma = 0, cuantos = 0, ignorados = 0;
try (Scanner sc = new Scanner(fichero)) { // Scanner sobre un fichero: sí se cierra
while (sc.hasNext()) {
if (sc.hasNextInt()) {
suma += sc.nextInt();
cuantos++;
} else {
System.out.println("Ignorado: " + sc.next());
ignorados++;
}
}
}
System.out.println(cuantos + " números, suma " + suma + ", " + ignorados + " ignorados");
}
}
Salida:
Ignorado: x
6 números, suma 80, 1 ignorados
A diferencia del Scanner(System.in), un Scanner sobre un fichero sí debe cerrarse (con try-with-resources). hasNextInt() permite comprobar el tipo antes de leer y evitar la InputMismatchException. Para leer líneas de texto, en cambio, BufferedReader es más rápido y predecible.
8. Ficheros binarios: DataOutputStream y DataInputStream
En un fichero binario, los datos se guardan en su formato interno: un int ocupa siempre 4 bytes y un double, 8, tengan el valor que tengan. Las clases DataOutputStream y DataInputStream escriben y leen tipos primitivos y String en ese formato:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Binario {
public static void main(String[] args) throws IOException {
Path fichero = Path.of("productos.dat");
String[] nombres = {"Teclado", "Ratón", "Monitor"};
int[] stock = {12, 40, 3};
double[] precios = {25.5, 9.99, 189.0};
// Escribir: cada dato en su formato binario
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(Files.newOutputStream(fichero)))) {
for (int i = 0; i < nombres.length; i++) {
out.writeUTF(nombres[i]); // 2 bytes de longitud + el texto
out.writeInt(stock[i]); // 4 bytes
out.writeDouble(precios[i]); // 8 bytes
}
}
System.out.println("Tamaño de " + fichero + ": " + Files.size(fichero) + " bytes");
// Leer: EN EL MISMO ORDEN y con los mismos tipos
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(Files.newInputStream(fichero)))) {
while (true) {
String nombre = in.readUTF();
int unidades = in.readInt();
double precio = in.readDouble();
System.out.printf(" %-8s %3d uds %7.2f €%n", nombre, unidades, precio);
}
} catch (EOFException e) {
System.out.println("(fin del fichero)");
}
// ¿Qué hay realmente dentro? Los primeros bytes en hexadecimal
byte[] bytes = Files.readAllBytes(fichero);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 21; i++) {
sb.append(String.format("%02X ", bytes[i]));
}
System.out.println("Primeros 21 bytes: " + sb);
}
}
Salida:
Tamaño de productos.dat: 62 bytes
Teclado 12 uds 25.50 €
Ratón 40 uds 9.99 €
Monitor 3 uds 189.00 €
(fin del fichero)
Primeros 21 bytes: 00 07 54 65 63 6C 61 64 6F 00 00 00 0C 40 39 80 00 00 00 00 00
Los primeros 21 bytes son el primer producto: 00 07 (la longitud de “Teclado”), 54 65 63 6C 61 64 6F (las letras T, e, c, l, a, d, o), 00 00 00 0C (el int 12) y 40 39 80 00 00 00 00 00 (el double 25.5). “Ratón” ocupa un byte más que sus 5 letras porque la ó necesita dos bytes.
| Escribir | Leer | Bytes |
|---|---|---|
writeInt(n) | readInt() | 4 |
writeLong(n) | readLong() | 8 |
writeDouble(d) | readDouble() | 8 |
writeBoolean(b) | readBoolean() | 1 |
writeUTF(s) | readUTF() | 2 + longitud del texto |
⚠️ Importante: Un fichero binario no tiene estructura visible: hay que leerlo exactamente en el mismo orden y con los mismos tipos con que se escribió. Si lees un
doubledonde había unint, no hay error: obtienes un número sin sentido. Cuando se intenta leer más allá del final, se lanzaEOFException, que puede usarse para detectar el fin, como en el ejemplo, o evitarse guardando al principio cuántos registros hay (ejercicio 6).
8.1. Ejemplo: copia de ficheros
Para copiar cualquier fichero (una foto, un PDF), se trabaja con bytes: un InputStream lee bloques en un byte[] y un OutputStream los escribe. Así funciona por dentro Files.copy:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class CopiaFicheros {
// Copia "a mano" con un buffer de bytes: funciona con cualquier tipo de fichero
static long copiar(Path origen, Path destino) throws IOException {
long total = 0;
try (InputStream in = Files.newInputStream(origen);
OutputStream out = Files.newOutputStream(destino)) {
byte[] buffer = new byte[8192];
int leidos;
while ((leidos = in.read(buffer)) != -1) { // -1 = fin del fichero
out.write(buffer, 0, leidos);
total += leidos;
}
}
return total;
}
public static void main(String[] args) throws IOException {
Path original = Path.of("foto.bin");
byte[] datos = new byte[100_000];
for (int i = 0; i < datos.length; i++) {
datos[i] = (byte) (i * 31); // contenido "binario" de prueba
}
Files.write(original, datos);
long bytes = copiar(original, Path.of("copia1.bin"));
System.out.println("Copia manual: " + bytes + " bytes");
Files.copy(original, Path.of("copia2.bin"), StandardCopyOption.REPLACE_EXISTING);
System.out.println("Files.copy: " + Files.size(Path.of("copia2.bin")) + " bytes");
// Comprobar que las copias son idénticas (Java 12+)
System.out.println("¿Idénticas? " + (Files.mismatch(original, Path.of("copia1.bin")) == -1)
+ " y " + (Files.mismatch(original, Path.of("copia2.bin")) == -1));
}
}
Salida:
Copia manual: 100000 bytes
Files.copy: 100000 bytes
¿Idénticas? true y true
in.read(buffer) rellena el array y devuelve cuántos bytes ha leído (puede ser menos que el tamaño del array en el último bloque) o -1 al final. Por eso se escribe out.write(buffer, 0, leidos) y no el array entero. Files.mismatch (Java 12) compara dos ficheros y devuelve -1 si son idénticos.
9. Serialización de objetos
9.1. Guardar objetos “tal cual”
La serialización convierte un objeto (con todos los objetos a los que referencia) en una secuencia de bytes que se puede guardar en un fichero y, más tarde, deserializar para reconstruir el objeto. Para que una clase se pueda serializar, debe implementar la interfaz Serializable, que no tiene métodos: es solo una “marca”.
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
class Usuario implements Serializable {
@Serial
private static final long serialVersionUID = 1L; // versión de la "forma" de la clase
private final String login;
private final int nivel;
private transient String contrasena; // transient: NO se guarda
Usuario(String login, int nivel, String contrasena) {
this.login = login;
this.nivel = nivel;
this.contrasena = contrasena;
}
@Override
public String toString() {
return login + " (nivel " + nivel + ", contraseña " + contrasena + ")";
}
}
public class Serializacion {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Path fichero = Path.of("usuarios.ser");
List<Usuario> usuarios = new ArrayList<>(List.of(
new Usuario("admin", 9, "s3cr3t0"), new Usuario("invitado", 1, "1234")));
System.out.println("Antes de guardar: " + usuarios);
try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(fichero))) {
out.writeObject(usuarios); // guarda la lista y todo lo que contiene
}
System.out.println("Guardados en " + fichero + " (" + Files.size(fichero) + " bytes)");
try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(fichero))) {
@SuppressWarnings("unchecked")
List<Usuario> leidos = (List<Usuario>) in.readObject();
System.out.println("Leídos: " + leidos);
}
}
}
Salida:
Antes de guardar: [admin (nivel 9, contraseña s3cr3t0), invitado (nivel 1, contraseña 1234)]
Guardados en usuarios.ser (152 bytes)
Leídos: [admin (nivel 9, contraseña null), invitado (nivel 1, contraseña null)]
| Elemento | Para qué sirve |
|---|---|
implements Serializable | Permite serializar la clase. Todos sus atributos deben ser también serializables (String, Integer, ArrayList… lo son) |
transient | Marca un atributo que no se guarda. Al leer, queda con su valor por defecto (null, 0, false). Úsalo para contraseñas, cachés o datos que se pueden recalcular |
serialVersionUID | Número de versión de la clase. Si al leer no coincide con el de la clase actual, se lanza InvalidClassException |
@Serial (Java 14) | Anotación opcional que pide al compilador comprobar que el campo está bien declarado |
writeObject / readObject | Guardan y leen un objeto completo; readObject devuelve Object, así que hay que hacer un cast |
9.2. Riesgos de la serialización
La serialización parece la solución perfecta (¡una línea para guardar toda la agenda!), pero tiene inconvenientes serios:
- Fragilidad: si cambias la clase (añades o quitas un atributo, la renombras, la mueves de paquete), los ficheros antiguos pueden dejar de leerse.
- Formato cerrado: el fichero solo lo entiende un programa Java con esas mismas clases. No lo puedes abrir con un editor, con Excel ni desde otro lenguaje.
- Seguridad: deserializar datos de origen no confiable (un fichero descargado, datos que llegan por la red) ha sido la causa de graves vulnerabilidades en aplicaciones Java, porque se pueden construir bytes que, al deserializarse, ejecutan código no deseado.
💡 Consejo: Para guardar los datos de tus aplicaciones, prefiere formatos de texto (CSV, o JSON en el Curso Avanzado) o una base de datos. Usa la serialización solo para datos propios y temporales, y nunca deserialices datos que no hayas generado tú.
10. La clase File (heredada)
Antes de Java 7, las rutas y las operaciones sobre ficheros se hacían con la clase java.io.File. Sigue existiendo y la verás en código antiguo y en muchas bibliotecas:
import java.io.File;
import java.nio.file.Path;
public class FileHeredada {
public static void main(String[] args) throws Exception {
File carpeta = new File("docs");
carpeta.mkdirs();
File f = new File(carpeta, "nota.txt");
System.out.println("createNewFile: " + f.createNewFile());
System.out.println(f.getName() + " existe=" + f.exists() + " tamaño=" + f.length()
+ " ruta=" + f.getPath());
Path p = f.toPath(); // de File a Path...
File otra = p.toFile(); // ...y de Path a File
System.out.println("Convertido: " + p + " / " + otra);
System.out.println("delete: " + f.delete() + " (devuelve false si falla, sin excepción)");
System.out.println("delete otra vez: " + f.delete());
}
}
Salida:
createNewFile: true
nota.txt existe=true tamaño=0 ruta=docs/nota.txt
Convertido: docs/nota.txt / docs/nota.txt
delete: true (devuelve false si falla, sin excepción)
delete otra vez: false
java.io.File (antigua) | java.nio.file.Path + Files (moderna) | |
|---|---|---|
| Errores | Muchos métodos devuelven false sin decir por qué | Lanzan excepciones con información (NoSuchFileException…) |
| Funcionalidad | Básica | Copiar, mover, atributos, enlaces, codificación… |
| Conversión | file.toPath() | path.toFile() |
💡 Consejo: En código nuevo, usa
PathyFiles. Si una biblioteca te pide unFile, conviértelo conpath.toFile().
11. Ejemplo completo: estadísticas de un fichero de texto
Este programa analiza un fichero línea a línea y combina casi todo lo visto en el curso: ficheros, cadenas, expresiones regulares, mapas y bucles:
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class Estadisticas {
public static void main(String[] args) throws IOException {
Path fichero = Path.of("quijote.txt");
Files.writeString(fichero, """
En un lugar de la Mancha, de cuyo nombre no quiero acordarme,
no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero,
adarga antigua, rocín flaco y galgo corredor.
Una olla de algo más vaca que carnero, salpicón las más noches,
duelos y quebrantos los sábados, lentejas los viernes,
algún palomino de añadidura los domingos, consumían las tres partes de su hacienda.
""");
int lineas = 0, vacias = 0, palabras = 0, caracteres = 0;
String masLarga = "";
Map<String, Integer> frecuencia = new HashMap<>();
try (BufferedReader in = Files.newBufferedReader(fichero)) {
String linea;
while ((linea = in.readLine()) != null) {
lineas++;
caracteres += linea.length();
if (linea.isBlank()) {
vacias++;
continue;
}
if (linea.length() > masLarga.length()) {
masLarga = linea;
}
for (String p : linea.toLowerCase().split("[^\\p{L}]+")) {
if (!p.isEmpty()) {
palabras++;
frecuencia.put(p, frecuencia.getOrDefault(p, 0) + 1);
}
}
}
}
String top = null;
for (Map.Entry<String, Integer> e : frecuencia.entrySet()) {
if (top == null || e.getValue() > frecuencia.get(top)
|| (e.getValue().equals(frecuencia.get(top)) && e.getKey().compareTo(top) < 0)) {
top = e.getKey();
}
}
System.out.println("Fichero: " + fichero + " (" + Files.size(fichero) + " bytes)");
System.out.println("Líneas: " + lineas + " (" + vacias + " vacías)");
System.out.println("Palabras: " + palabras + " (" + frecuencia.size() + " distintas)");
System.out.println("Caracteres (sin saltos de línea): " + caracteres);
System.out.println("Línea más larga (" + masLarga.length() + "): " + masLarga);
System.out.println("Palabra más frecuente: \"" + top + "\" (" + frecuencia.get(top) + " veces)");
}
}
Salida:
Fichero: quijote.txt (391 bytes)
Líneas: 7 (1 vacías)
Palabras: 66 (50 distintas)
Caracteres (sin saltos de línea): 375
Línea más larga (83): algún palomino de añadidura los domingos, consumían las tres partes de su hacienda.
Palabra más frecuente: "de" (7 veces)
12. Buenas prácticas y errores comunes
| Error | Consecuencia | Solución |
|---|---|---|
| No cerrar el fichero | Datos sin escribir, fichero bloqueado | Try-with-resources siempre |
Rutas absolutas “a fuego” (C:\Users\Ana\...) | El programa no funciona en otro equipo | Rutas relativas (o configurables) con Path.of |
Concatenar rutas con "/" o "\\" | Falla en otro sistema operativo | Path.of("a", "b") o resolve |
| Leer con una codificación distinta de la usada al escribir | Tildes y eñes rotas | UTF-8 en todo, indicado explícitamente |
Olvidar APPEND | Se borra el contenido anterior | StandardOpenOption.CREATE, APPEND |
| Un error en una línea del CSV detiene toda la carga | Se pierden todos los datos | try/catch dentro del bucle |
split(";") con el último campo vacío | Faltan campos | split(";", -1) |
| Leer un binario en otro orden o con otros tipos | Datos absurdos, sin error | Mismo orden y mismos tipos que al escribir |
catch (IOException e) { } vacío | El usuario no sabe que no se ha guardado nada | Informa del error |
| Deserializar ficheros de origen desconocido | Riesgo de seguridad | Usa formatos de texto; deserializa solo lo tuyo |
13. Ejercicios
Ejercicio 1: Diario personal
Escribe un programa con dos métodos: anotar(Path diario, String texto), que añada una entrada al final del fichero (creándolo si no existe), y mostrar(Path diario), que muestre todas las entradas numeradas o “(diario vacío)” si el fichero no existe todavía.
Ver solución
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
public class E1Diario {
static void anotar(Path diario, String texto) throws IOException {
String linea = "- " + texto + System.lineSeparator();
Files.writeString(diario, linea, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
static void mostrar(Path diario) throws IOException {
if (!Files.exists(diario)) {
System.out.println("(diario vacío)");
return;
}
List<String> lineas = Files.readAllLines(diario);
System.out.println("Diario (" + lineas.size() + " entradas):");
for (int i = 0; i < lineas.size(); i++) {
System.out.println(" " + (i + 1) + " " + lineas.get(i));
}
}
public static void main(String[] args) throws IOException {
Path diario = Path.of("diario.txt");
mostrar(diario);
anotar(diario, "Hoy he aprendido a leer ficheros");
anotar(diario, "Mañana, CSV y serialización");
mostrar(diario);
anotar(diario, "Repaso de excepciones");
mostrar(diario);
}
}
Salida (en la primera ejecución, con el fichero todavía sin crear):
(diario vacío)
Diario (2 entradas):
1 - Hoy he aprendido a leer ficheros
2 - Mañana, CSV y serialización
Diario (3 entradas):
1 - Hoy he aprendido a leer ficheros
2 - Mañana, CSV y serialización
3 - Repaso de excepciones
System.lineSeparator() devuelve el salto de línea del sistema operativo. Si ejecutas el programa una segunda vez, el diario empezará con 3 entradas y terminará con 6, porque APPEND conserva lo anterior: eso es persistencia.
Ejercicio 2: Buscar en un fichero
Escribe int buscar(Path origen, String palabra, Path resultado) que copie en resultado las líneas de origen que contengan la palabra (sin distinguir mayúsculas), precedidas por su número de línea, y devuelva cuántas ha encontrado. Comprueba qué ocurre si el fichero de origen no existe.
Ver solución
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class E2Buscar {
static int buscar(Path origen, String palabra, Path resultado) throws IOException {
int encontradas = 0;
try (BufferedReader in = Files.newBufferedReader(origen);
BufferedWriter out = Files.newBufferedWriter(resultado)) {
String linea;
int numero = 0;
while ((linea = in.readLine()) != null) {
numero++;
if (linea.toLowerCase().contains(palabra.toLowerCase())) {
out.write(numero + ": " + linea);
out.newLine();
encontradas++;
}
}
}
return encontradas;
}
public static void main(String[] args) throws IOException {
Path origen = Path.of("refranes.txt");
Files.writeString(origen, """
A quien madruga, Dios le ayuda.
No por mucho madrugar amanece más temprano.
Más vale pájaro en mano que ciento volando.
Camarón que se duerme se lo lleva la corriente.
""");
Path resultado = Path.of("resultado.txt");
int n = buscar(origen, "MADRUG", resultado);
System.out.println(n + " líneas encontradas. Contenido de " + resultado + ":");
System.out.print(Files.readString(resultado));
try {
buscar(Path.of("no-existe.txt"), "x", resultado);
} catch (IOException e) {
System.out.println("Error: " + e.getClass().getSimpleName() + " " + e.getMessage());
}
}
}
Salida:
2 líneas encontradas. Contenido de resultado.txt:
1: A quien madruga, Dios le ayuda.
2: No por mucho madrugar amanece más temprano.
Error: NoSuchFileException no-existe.txt
Un único try-with-resources abre los dos ficheros y garantiza que ambos se cierran. El método no captura la IOException: la declara con throws y deja que decida quien lo llama (aquí, main informa del error). Es lo correcto según el módulo 15: buscar no sabe si el usuario quiere reintentar, elegir otro fichero o cancelar.
Ejercicio 3: Informe de notas desde CSV
Lee un fichero notas.csv con el formato alumno;t1;t2;t3, calcula la media de cada alumno y genera informe.csv con las columnas alumno;media;resultado (APROBADO si la media es al menos 5). Las líneas con errores deben avisarse y no aparecer en el informe.
Ver solución
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
public class E3Informe {
public static void main(String[] args) throws IOException {
Path notas = Path.of("notas.csv");
Files.writeString(notas, """
alumno;t1;t2;t3
Laura;7.5;8;9
Óscar;4;5.5;3
Nerea;6;x;7
Iker;9;9.5
Carmen;5;5;5
""");
List<String> lineas = Files.readAllLines(notas);
int aprobados = 0, procesados = 0;
try (BufferedWriter out = Files.newBufferedWriter(Path.of("informe.csv"))) {
out.write("alumno;media;resultado");
out.newLine();
for (int i = 1; i < lineas.size(); i++) { // i = 1: saltar cabecera
String[] c = lineas.get(i).split(";");
try {
if (c.length != 4) {
throw new IllegalArgumentException(c.length + " campos");
}
double media = (Double.parseDouble(c[1]) + Double.parseDouble(c[2])
+ Double.parseDouble(c[3])) / 3;
String resultado = media >= 5 ? "APROBADO" : "SUSPENSO";
out.write(String.format(Locale.ROOT, "%s;%.2f;%s", c[0], media, resultado));
out.newLine();
procesados++;
if (media >= 5) {
aprobados++;
}
} catch (IllegalArgumentException e) {
System.out.println("Línea " + (i + 1) + " con errores (" + e.getMessage() + "): " + lineas.get(i));
}
}
}
System.out.println(procesados + " alumnos procesados, " + aprobados + " aprobados");
System.out.print(Files.readString(Path.of("informe.csv")));
}
}
Salida:
Línea 4 con errores (For input string: "x"): Nerea;6;x;7
Línea 5 con errores (3 campos): Iker;9;9.5
3 alumnos procesados, 2 aprobados
alumno;media;resultado
Laura;8.17;APROBADO
Óscar;4.17;SUSPENSO
Carmen;5.00;APROBADO
String.format(Locale.ROOT, "%.2f", media) garantiza el punto decimal (8.17): con la configuración española de printf saldría 8,17, y al volver a leer el fichero con Double.parseDouble fallaría. Una línea con menos campos se detecta comprobando c.length y lanzando una IllegalArgumentException que recoge el mismo catch que los errores de conversión.
Ejercicio 4: Fichero de configuración
Muchos programas guardan su configuración en ficheros de texto con líneas clave = valor. Escribe Map<String, String> leerConfig(Path) que ignore las líneas vacías y los comentarios (que empiezan por #), avise de las líneas mal formadas y elimine los espacios alrededor de claves y valores. Después, lee valores con un valor por defecto si no existen.
Ver solución
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
public class E4Config {
static Map<String, String> leerConfig(Path fichero) throws IOException {
Map<String, String> config = new LinkedHashMap<>();
int n = 0;
for (String linea : Files.readAllLines(fichero)) {
n++;
linea = linea.strip();
if (linea.isEmpty() || linea.startsWith("#")) {
continue; // vacía o comentario
}
int igual = linea.indexOf('=');
if (igual <= 0) {
System.out.println("Aviso: línea " + n + " ignorada: " + linea);
continue;
}
config.put(linea.substring(0, igual).strip(), linea.substring(igual + 1).strip());
}
return config;
}
public static void main(String[] args) throws IOException {
Path fichero = Path.of("app.conf");
Files.writeString(fichero, """
# Configuración de la aplicación
idioma = es
tema=oscuro
tamano.letra = 14
esto no es válido
ruta.datos = /home/alumno/datos
""");
Map<String, String> config = leerConfig(fichero);
System.out.println(config);
int tamano = Integer.parseInt(config.getOrDefault("tamano.letra", "12"));
String sonido = config.getOrDefault("sonido", "activado");
System.out.println("Tamaño de letra: " + tamano + ", sonido: " + sonido);
}
}
Salida:
Aviso: línea 6 ignorada: esto no es válido
{idioma=es, tema=oscuro, tamano.letra=14, ruta.datos=/home/alumno/datos}
Tamaño de letra: 14, sonido: activado
Se usa indexOf('=') en lugar de split("=") para que un valor pueda contener = (por ejemplo, formula = a=b). LinkedHashMap conserva el orden del fichero. Java incluye la clase java.util.Properties, que hace algo muy parecido con ficheros .properties; aquí lo programamos a mano para practicar.
Ejercicio 5: Copia de seguridad
Escribe copiaDeSeguridad(Path origen, Path destino, String patron) que copie a la carpeta destino (creándola si hace falta) todos los ficheros de origen que cumplan un patrón como *.txt, sobrescribiendo las copias anteriores, y muestre cuántos ficheros y bytes ha copiado.
Ver solución
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class E5Backup {
static int copiaDeSeguridad(Path origen, Path destino, String patron) throws IOException {
Files.createDirectories(destino);
int copiados = 0;
long bytes = 0;
try (DirectoryStream<Path> ficheros = Files.newDirectoryStream(origen, patron)) {
for (Path f : ficheros) {
if (Files.isRegularFile(f)) {
Path copia = destino.resolve(f.getFileName());
Files.copy(f, copia, StandardCopyOption.REPLACE_EXISTING);
copiados++;
bytes += Files.size(copia);
System.out.println(" copiado " + f.getFileName());
}
}
}
System.out.println(copiados + " ficheros, " + bytes + " bytes");
return copiados;
}
public static void main(String[] args) throws IOException {
Path docs = Path.of("documentos");
Files.createDirectories(docs.resolve("subcarpeta"));
Files.writeString(docs.resolve("notas.txt"), "Apuntes de Java");
Files.writeString(docs.resolve("tareas.txt"), "1. Estudiar ficheros\n2. Hacer ejercicios\n");
Files.writeString(docs.resolve("foto.png"), "no es una foto de verdad");
System.out.println("Copia de *.txt:");
copiaDeSeguridad(docs, Path.of("backup"), "*.txt");
System.out.println("Segunda copia (sobrescribe):");
copiaDeSeguridad(docs, Path.of("backup"), "*.txt");
}
}
Salida:
Copia de *.txt:
copiado notas.txt
copiado tareas.txt
2 ficheros, 56 bytes
Segunda copia (sobrescribe):
copiado notas.txt
copiado tareas.txt
2 ficheros, 56 bytes
Files.newDirectoryStream(carpeta, "*.txt") filtra por un patrón glob (* = cualquier texto), así que foto.png no se copia. isRegularFile descarta las subcarpetas. REPLACE_EXISTING permite repetir la copia; sin esa opción, la segunda ejecución lanzaría FileAlreadyExistsException.
Ejercicio 6: Temperaturas en binario
Guarda las temperaturas de una semana en un fichero binario: primero un int con cuántas hay y después cada temperatura como double. Escribe el método que lo carga en un array, muestra la mínima, la máxima y la media, y comprueba qué ocurre al leer un fichero truncado.
Ver solución
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class E6Temperaturas {
static void guardar(Path fichero, double[] temperaturas) throws IOException {
try (DataOutputStream out = new DataOutputStream(Files.newOutputStream(fichero))) {
out.writeInt(temperaturas.length); // primero, cuántas hay
for (double t : temperaturas) {
out.writeDouble(t);
}
}
}
static double[] cargar(Path fichero) throws IOException {
try (DataInputStream in = new DataInputStream(Files.newInputStream(fichero))) {
int n = in.readInt();
double[] datos = new double[n];
for (int i = 0; i < n; i++) {
datos[i] = in.readDouble();
}
return datos;
}
}
public static void main(String[] args) throws IOException {
Path fichero = Path.of("semana.dat");
guardar(fichero, new double[] {14.5, 16.0, 18.25, 17.5, 12.0, 11.75, 15.0});
System.out.println("Tamaño: " + Files.size(fichero) + " bytes (4 + 7 × 8)");
double[] t = cargar(fichero);
double min = t[0], max = t[0], suma = 0;
for (double v : t) {
min = Math.min(min, v);
max = Math.max(max, v);
suma += v;
}
System.out.printf("Días: %d, mínima %.2f, máxima %.2f, media %.2f%n", t.length, min, max, suma / t.length);
// Un fichero truncado provoca EOFException
byte[] bytes = Files.readAllBytes(fichero);
Files.write(Path.of("roto.dat"), java.util.Arrays.copyOf(bytes, 20));
try {
cargar(Path.of("roto.dat"));
} catch (EOFException e) {
System.out.println("Fichero incompleto: " + e);
}
}
}
Salida:
Tamaño: 60 bytes (4 + 7 × 8)
Días: 7, mínima 11.75, máxima 18.25, media 15.00
Fichero incompleto: java.io.EOFException
Guardar primero el número de elementos permite crear el array del tamaño exacto y leer con un for, sin depender de EOFException. El tamaño del fichero es predecible: 4 bytes del int más 7 × 8 de los double. Al cortar el fichero a 20 bytes, solo caben el int y dos temperaturas, y la tercera lectura lanza EOFException.
Ejercicios propuestos
✏️ Ejercicio 7: Agenda con menú. Convierte la agenda de la sección 6.3 en un programa de consola con un menú (añadir, buscar, borrar, listar, salir) que cargue
agenda.csval arrancar y lo guarde al salir. Valida la entrada con lo aprendido en el módulo 15.
✏️ Ejercicio 8: Contador de visitas. Escribe un programa que, cada vez que se ejecute, lea un número de un fichero
contador.txt(0 si no existe), lo incremente, lo guarde y muestre “Esta es la ejecución número N”.
✏️ Ejercicio 9: Ficheros más grandes. Escribe un programa que reciba una carpeta y muestre sus ficheros ordenados de mayor a menor tamaño (usa
newDirectoryStream, una lista y unComparator), con el tamaño en KB y el total al final.
✏️ Ejercicio 10: De CSV a informe con formato. Lee un CSV de productos (
codigo;nombre;precio;unidades) y genera un fichero de texto con una tabla alineada conprintf/String.format, una línea de total y el producto con más valor en stock.
✏️ Ejercicio 11: Inventario serializado. Crea una clase
Producto implements Serializablecon un atributotransientpara la fecha de la última consulta. Guarda unaArrayList<Producto>conObjectOutputStream, léela de nuevo y comprueba qué valor tiene el atributotransient. Después, añade un atributo nuevo a la clase sin cambiar elserialVersionUIDe intenta leer el fichero antiguo: ¿qué ocurre? ¿Y si cambias elserialVersionUID?
✏️ Ejercicio 12: Comparar ficheros. Escribe un programa que compare dos ficheros de texto línea a línea y muestre el número y el contenido de las líneas que difieren, y las líneas que sobran en el más largo.
Resumen
| Concepto | Sintaxis / ejemplo | Idea clave |
|---|---|---|
| Ruta | Path p = Path.of("datos", "a.txt"); | Relativa al directorio de trabajo; no toca el disco |
| Operaciones con rutas | resolve, getFileName, getParent, toAbsolutePath, normalize | Construir y analizar rutas |
| Leer/escribir todo | Files.readString(p), Files.writeString(p, s) | Ficheros pequeños, UTF-8 por defecto |
| Líneas | Files.readAllLines(p), Files.write(p, lista) | Lista de String |
| Añadir al final | StandardOpenOption.CREATE, StandardOpenOption.APPEND | Sin APPEND, se sobrescribe |
| Gestión | exists, createDirectories, copy, move, delete, newDirectoryStream | Casi todo lanza IOException |
| Texto con buffer | Files.newBufferedReader/Writer(p), readLine(), newLine() | Siempre con try-with-resources |
| CSV | linea.split(";", -1), String.join(";", ...) | aCsv() / desdeCsv(); errores línea a línea |
Scanner sobre fichero | new Scanner(path), hasNextInt() | Cerrarlo (a diferencia de System.in) |
| Binario | DataOutputStream.writeInt/writeDouble/writeUTF | Leer en el mismo orden y tipos; EOFException |
| Bytes | InputStream.read(buffer), OutputStream.write(buffer, 0, n) | Copiar cualquier fichero |
| Serialización | implements Serializable, transient, serialVersionUID | Cómoda pero frágil e insegura con datos ajenos |
| Clase antigua | java.io.File, file.toPath() | Usa Path y Files en código nuevo |
| Errores | NoSuchFileException, FileAlreadyExistsException, EOFException | Todas son IOException (comprobadas) |