9. Ficheros en Java
Imagínate que tu aplicación es un juego tipo RPG online: cada vez que un jugador guarda la partida, no quieres que sus datos desaparezcan cuando cierre el juego. Necesitas un "punto de guardado" persistente. En el mundo real, ese "punto de guardado" son los ficheros o una base de datos. En este tema vas a ver cómo Java habla con el sistema de archivos para guardar, leer y transformar datos de forma profesional, igual que haría un backend real de una web o un microservicio.
Además, conectarás con lo visto en el tema de Programación Funcional y Streams, porque muchos ejemplos combinarán ficheros con Stream de Java para procesar datos de forma compacta y elegante.
Conceptos Fundamentales
Definición: Fichero
Un fichero es una secuencia de datos almacenada en un dispositivo (disco, SSD, USB, red) gestionada por el sistema de archivos, identificada por una ruta y un nombre.
Definición: Fichero de texto
Un fichero de texto almacena datos como caracteres con una codificación (por ejemplo UTF‑8), de forma legible para humanos y herramientas como editores.
Definición: Fichero binario
Un fichero binario almacena datos como bytes sin interpretación directa como caracteres, pensado para eficiencia y no para lectura humana.
Definición: Stream o Flujo
Un stream es una corriente secuencial de datos de entrada o salida que permite leer o escribir bytes o caracteres desde/hacia una fuente o destino (fichero, socket, memoria, etc.).
Definición: Acceso secuencial
El acceso secuencial obliga a leer o escribir datos en orden, desde el principio hacia adelante sin saltos directos a posiciones arbitrarias.
Definición: Acceso aleatorio
El acceso aleatorio permite saltar a una posición concreta del fichero para leer o escribir sin procesar todo lo anterior, usando posiciones (offsets).
Definición: Serialización
La serialización es el proceso de convertir el estado de un objeto en una secuencia de bytes para almacenarlo o enviarlo, y poder reconstruirlo después mediante deserialización.
Definición: DTO
Un DTO (Data Transfer Object) es un objeto simple que transporta datos entre capas o sistemas, normalmente sin lógica de negocio, usado para intercambiar información (JSON, XML, etc.).
Desarrollo y Ejemplos Prácticos
1. Introducción a los ficheros
1.1 ¿Qué es un fichero?
Un fichero, a nivel del sistema operativo, es como una "carpeta de sobres" alineados uno detrás de otro, donde cada sobre contiene datos binarios. Java no ve "documentos de Word", ve secuencias de bytes en rutas como C:\data\users.dat o /home/alumno/data/users.txt.
En Java, para trabajar con un fichero primero se localiza (ruta), luego se abre a través de un stream, se lee/escribe y finalmente se cierra.
Ejemplo 1 – Comprobando existencia de un fichero (API moderna Path / Files):
import java.nio.file.Files;
import java.nio.file.Path;
public class CheckFileExample {
public static void main(String[] args) {
Path path = Path.of("data", "usuarios.txt");
if (Files.exists(path)) {
System.out.println("El fichero existe: " + path.toAbsolutePath());
} else {
System.out.println("El fichero NO existe.");
}
}
}
Ejemplo 2 – Creando un fichero vacío si no existe:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CreateFileExample {
public static void main(String[] args) {
Path path = Path.of("logs", "app.log");
try {
if (Files.notExists(path)) {
Files.createDirectories(path.getParent());
Files.createFile(path);
System.out.println("Fichero creado en: " + path.toAbsolutePath());
} else {
System.out.println("El fichero ya existe.");
}
} catch (IOException e) {
System.err.println("Error creando el fichero: " + e.getMessage());
}
}
}
flowchart LR
A["Ruta lógica<br/>\'logs/app.log\'"] --> B["Objeto Path"]
B --> C["Comprobar existencia<br/>Files.exists"]
C -->|No existe| D["Crear directorios<br/>Files.createDirectories"]
D --> E["Crear fichero<br/>Files.createFile"]
C -->|Existe| F["Usar fichero<br/>lectura/escritura"]
Analogía
Un fichero es como una hoja de cálculo guardada en tu disco: la app (Java) no "ve" celdas, ve una secuencia de bytes que luego interpreta con un formato concreto.
Preguntas / ejercicios
Question
- ¿Por qué es importante cerrar siempre un fichero después de usarlo?
- ¿Qué diferencias ves entre la ruta lógica (
"logs/app.log") y la ruta absoluta? - ¿Qué podría ocurrir si intentas crear un fichero en una carpeta que no existe?
- Piensa en una aplicación web: ¿qué datos tendría sentido persistir en ficheros y cuáles en BBDD?
1.2 Tipos de ficheros: texto y binarios
Los ficheros de texto guardan información como caracteres (char), mientras que los binarios guardan directamente representaciones de tipos primitivos u objetos. La elección afecta a portabilidad, tamaño y rendimiento.
Tabla comparativa:
| Característica | Texto | Binario |
|---|---|---|
| Legibilidad humana | Sí | No |
| Eficiencia espacio/tiempo | Menor, requiere codificación caracteres | Mayor, datos en formato compacto |
| Portabilidad | Alta (con codificación conocida) | Menor, dependiente de plataforma/implementación |
| Uso típico | Configuración, logs, CSV, JSON | Imágenes, audio, objetos serializados |
Ejemplo 1 – Guardando un entero como texto:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class SaveIntAsText {
public static void main(String[] args) throws IOException {
int score = 123456789;
Path path = Path.of("data", "score.txt");
Files.createDirectories(path.getParent());
Files.writeString(path, String.valueOf(score), StandardCharsets.UTF_8);
}
}
Ejemplo 2 – Guardando un entero en binario:
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
public class SaveIntAsBinary {
public static void main(String[] args) {
int score = 123456789;
Path path = Path.of("data", "score.bin");
try (DataOutputStream out =
new DataOutputStream(new FileOutputStream(path.toFile()))) {
out.writeInt(score);
} catch (IOException e) {
System.err.println("Error escribiendo binario: " + e.getMessage());
}
}
}
Warning
Si abres el binario con un editor de texto, verás "jeroglíficos". Eso no significa que esté corrupto, simplemente no es texto.
graph TD
A["Dato en memoria<br/>int score"] --> B["Texto<br/>String.valueOf"]
B --> C["Escritura en fichero<br/>UTF-8"]
A --> D["Binario<br/>writeInt"]
D --> E["Fichero binario"]
Preguntas / ejercicios
Question
- ¿Por qué un mismo entero ocupa más bytes en texto que en binario?
- ¿Qué tipo de fichero usarías para guardar logs de una aplicación y por qué?
- ¿Qué problemas de portabilidad pueden aparecer con ficheros binarios?
- Diseña un caso de uso real donde prefieras texto sobre binario aunque ocupe más.
1.3 Flujos (Streams) de entrada y salida
En Java, la E/S se hace a través de streams (flujo de bytes o caracteres). Existen flujos de entrada (lectura) y salida (escritura). En la API hay dos grandes familias: InputStream/OutputStream (bytes) y Reader/Writer (caracteres).
Idea clave
Los streams son como tuberías: puedes conectar varias para transformar o mejorar el flujo (buffer, filtrado, conversión de bytes a caracteres, etc.).
Ejemplo 1 – Lectura de bytes con InputStream:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ByteStreamExample {
public static void main(String[] args) {
String fileName = "data/raw.bin";
try (InputStream in = new FileInputStream(fileName)) {
int value;
while ((value = in.read()) != -1) {
System.out.println("Byte leído: " + value);
}
} catch (IOException e) {
System.err.println("Error leyendo fichero: " + e.getMessage());
}
}
}
Ejemplo 2 – De InputStream a caracteres con InputStreamReader:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
public class ReaderExample {
public static void main(String[] args) {
String fileName = "data/texto.txt";
try (Reader reader = new InputStreamReader(
new FileInputStream(fileName), StandardCharsets.UTF_8)) {
int ch;
while ((ch = reader.read()) != -1) {
System.out.print((char) ch);
}
} catch (IOException e) {
System.err.println("Error leyendo texto: " + e.getMessage());
}
}
}
flowchart LR
A[Disco] --> B["FileInputStream<br/>bytes"]
B --> C["InputStreamReader<br/>bytes→caracteres"]
C --> D["Código Java<br/>procesa texto"]
Preguntas / ejercicios
Question
- ¿Cuándo usarías
InputStreamdirectamente y cuándo preferirías unReader? - ¿Qué ventaja aporta separar en bytes y caracteres?
- ¿Qué puede pasar si lees texto UTF‑8 suponiendo ISO‑8859‑1?
- Diseña un caso real donde encadenes varios streams (por ejemplo, fichero comprimido y luego texto).
1.4 Entrada/Salida estándar
Además de ficheros, Java tiene tres streams especiales: System.in (entrada estándar), System.out (salida estándar) y System.err (errores). Estos permiten hacer pequeñas herramientas de consola o scripts de procesamiento.
Ejemplo 1 – Leer de entrada estándar con Scanner:
import java.util.Scanner;
public class EchoUppercase {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.println("Escribe texto (CTRL+D/CZ para terminar):");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line.toUpperCase());
}
}
}
}
Ejemplo 2 – Redirigir salida a fichero desde consola (uso típico):
Código:
public class GenerateReport {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
System.out.printf("Línea %d: informe%n", i);
}
}
}
Uso desde terminal (Linux/macOS):
java GenerateReport > report.txt
Tip
Redirigir la salida estándar es un truco clásico: tu programa solo "habla" por System.out, y quien lo ejecuta decide si lo ve en pantalla o lo guarda en un fichero.
Preguntas / ejercicios
Question
- ¿En qué tipo de utilidades de backend puede ser útil leer de
System.in? - ¿Qué diferencia funcional hay entre
System.outySystem.err? - ¿Por qué es interesante no "mezclar" logs y salida de datos en la misma corriente?
- Diseña una mini‐herramienta que lea CSV de
System.iny saque estadísticas porSystem.out.
1.5 Uso de try-catch con recursos (try-with-resources)
Trabajar con ficheros implica recursos del sistema operativo que hay que liberar. Para eso existe try-with-resources, que cierra automáticamente los recursos que implementan AutoCloseable.
Ejemplo 1 – Lectura segura con try-with-resources:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesRead {
public static void main(String[] args) {
String fileName = "data/users.txt";
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Usuario: " + line);
}
} catch (IOException e) {
System.err.println("Error de E/S: " + e.getMessage());
}
}
}
Ejemplo 2 – Varios recursos en la misma cabecera:
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class TryWithResourcesWrite {
public static void main(String[] args) {
String input = "Hola DAW!";
String fileName = "data/saludo.txt";
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
writer.write(input);
writer.newLine();
} catch (IOException e) {
System.err.println("Error escrito: " + e.getMessage());
}
}
}
sequenceDiagram
participant C as Código
participant R as Recurso (fichero)
C->>C: try-with-resources
C->>R: abrir()
C->>R: usar()
C->>R: (sale del bloque)
R-->>C: close() automático
Danger
No cerrar recursos puede bloquear archivos, dejar datos sin escribir en disco o agotar descriptores de fichero en el sistema operativo.
Preguntas / ejercicios
Question
- ¿Qué ventaja tiene
try-with-resourcesfrente a cerrar en un bloquefinallymanual? - Implementa un ejemplo donde olvides cerrar un fichero y observa el comportamiento.
- ¿Qué interfaz deben implementar los recursos para funcionar en
try-with-resources? - ¿Crees que es buena idea manejar manualmente
close()en código moderno?
2. Ficheros y Directorios en Java
2.1 Clase File
java.io.File es la clase "legacy" para representar rutas a ficheros y directorios. No lee ni escribe datos, solo representa la ruta y permite consultar metadatos (tamaño, si existe, si es directorio, etc.).
Ejemplo 1 – Información básica de un fichero:
import java.io.File;
public class FileInfoExample {
public static void main(String[] args) {
File file = new File("data", "usuarios.txt");
System.out.println("Nombre: " + file.getName());
System.out.println("Ruta absoluta: " + file.getAbsolutePath());
System.out.println("Existe: " + file.exists());
System.out.println("Es fichero: " + file.isFile());
System.out.println("Es directorio: " + file.isDirectory());
}
}
Ejemplo 2 – Listar ficheros de una carpeta:
import java.io.File;
public class ListFilesExample {
public static void main(String[] args) {
File dir = new File("data");
if (dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
System.out.printf("%s (%d bytes)%n",
f.getName(), f.length());
}
}
} else {
System.out.println("La ruta no es un directorio.");
}
}
}
Importante
En código moderno se prefiere Path / Files, pero File sigue apareciendo en muchas APIs existentes y exámenes.
Preguntas / ejercicios
Question
- ¿Qué limitaciones tiene
Filecomparado conPath? - Escribe un método que liste solo los
.txtde un directorio. - ¿Qué podría devolver
listFiles()si hay un error de permisos? - ¿Cómo obtendrías el directorio padre de un
File?
2.2 Clase Path
java.nio.file.Path representa rutas modernas y más robustas, con mejor soporte para sistemas de archivos y operaciones. Es inmutable y está pensada para usarse junto a la clase Files.
Ejemplo 1 – Creando y analizando un Path:
import java.nio.file.Path;
public class PathExample {
public static void main(String[] args) {
Path path = Path.of("data", "logs", "app.log");
System.out.println("Path: " + path);
System.out.println("Nombre del fichero: " + path.getFileName());
System.out.println("Elementos: " + path.getNameCount());
System.out.println("Padre: " + path.getParent());
System.out.println("Absoluto: " + path.toAbsolutePath());
}
}
Ejemplo 2 – Resolver rutas relativas:
import java.nio.file.Path;
public class ResolvePathExample {
public static void main(String[] args) {
Path base = Path.of("data");
Path relative = Path.of("images", "logo.png");
Path combined = base.resolve(relative);
System.out.println("Combinado: " + combined);
System.out.println("Normalizado: " + combined.normalize());
}
}
flowchart TD
A["Path base<br/>data"] --> B["resolve(images/logo.png)"]
B --> C["Path resultante<br/>data/images/logo.png"]
C --> D["toAbsolutePath()"]
Preguntas / ejercicios
Question
- ¿Qué ventajas tiene
Pathfrente aFile? - ¿Qué hace el método
normalize()sobre unPath? - Escribe un método que reciba un
Pathy muestre sus segmentos uno a uno. - ¿Cómo convertirías un
PathaFiley viceversa?
2.3 Clase Paths
Paths (plural) es una clase de utilidad para obtener instancias de Path. En Java recientes se usa más Path.of, pero Paths.get es aún muy común.
Ejemplo 1 – Uso de Paths.get:
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathsExample {
public static void main(String[] args) {
Path p1 = Paths.get("/tmp", "foo.log");
Path p2 = Paths.get("data/users.csv");
System.out.println(p1);
System.out.println(p2);
}
}
Ejemplo 2 – Comparando Path.of y Paths.get:
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathOfVsPathsGet {
public static void main(String[] args) {
Path p1 = Path.of("data", "config.properties");
Path p2 = Paths.get("data", "config.properties");
System.out.println("Son iguales: " + p1.equals(p2));
}
}
Tip
En proyectos nuevos, se recomienda Path.of(...), pero conocer Paths.get(...) te hará entender mejor ejemplos antiguos y documentación.
Preguntas / ejercicios
Question
- ¿Qué relación hay entre
Path,PathsyFileSystems? - ¿Qué estilo te parece más claro,
Path.ofoPaths.get? Justifica. - Escribe un método que reciba un
Stringy devuelva elPathusando ambas formas. - ¿Qué pasaría si pasas una URI a
Paths.get?
2.4 Clase Files
La clase Files ofrece operaciones estáticas para trabajar con ficheros y directorios: crear, copiar, mover, borrar, leer y escribir.
Ejemplo 1 – Crear directorio y fichero, escribir líneas:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class FilesWriteExample {
public static void main(String[] args) {
Path dir = Path.of("data");
Path file = dir.resolve("nombres.txt");
try {
Files.createDirectories(dir);
List<String> lines = List.of("Ana", "Pepe", "María");
Files.write(file, lines, StandardCharsets.UTF_8);
} catch (IOException e) {
System.err.println("Error escribiendo fichero: " + e.getMessage());
}
}
}
Ejemplo 2 – Leer líneas y procesar con Streams:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FilesReadStreamExample {
public static void main(String[] args) {
Path file = Path.of("data", "nombres.txt");
try {
Files.lines(file)
.filter(line -> !line.isBlank())
.map(String::toUpperCase)
.forEach(System.out::println);
} catch (IOException e) {
System.err.println("Error leyendo fichero: " + e.getMessage());
}
}
}
flowchart LR
A["Path fichero"] --> B["Files.lines"]
B --> C["Stream<String>"]
C --> D["filter / map"]
D --> E["forEach imprimir"]
Preguntas / ejercicios
Question
- ¿Qué diferencia hay entre
Files.writeStringyFiles.writecon lista de líneas? - Implementa una función que copie un fichero utilizando
Files.copy. - ¿Cómo borrarías un directorio vacío usando
Files? - Relaciona esto con el tema de Streams: ¿qué ventajas tiene procesar líneas como
Stream<String>?
2.5 Operaciones básicas con ficheros y directorios
Con Files se pueden copiar, mover, borrar y obtener atributos de ficheros.
Ejemplo 1 – Copiar y mover un fichero:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class CopyMoveExample {
public static void main(String[] args) {
Path source = Path.of("data", "nombres.txt");
Path backup = Path.of("backup", "nombres_backup.txt");
try {
Files.createDirectories(backup.getParent());
Files.copy(source, backup, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copia creada en " + backup);
Path moved = Path.of("data", "nombres_moved.txt");
Files.move(source, moved, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Fichero movido a " + moved);
} catch (IOException e) {
System.err.println("Error copiando/moviendo: " + e.getMessage());
}
}
}
Ejemplo 2 – Borrado seguro con comprobación:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class DeleteExample {
public static void main(String[] args) {
Path file = Path.of("data", "old.log");
try {
if (Files.deleteIfExists(file)) {
System.out.println("Fichero eliminado.");
} else {
System.out.println("El fichero no existía.");
}
} catch (IOException e) {
System.err.println("No se pudo borrar: " + e.getMessage());
}
}
}
Warning
Cuidado con Files.delete: si el fichero no existe lanza excepción. deleteIfExists es menos dramático.
Preguntas / ejercicios
Question
- ¿Qué opciones adicionales ofrece
StandardCopyOption? - Implementa un método que haga un backup de un directorio completo.
- ¿Cómo listarías recursivamente todos los ficheros de un árbol de directorios?
- ¿En qué casos preferirías
deleteIfExistsfrente adelete?
2.6 Directorio resources
En proyectos Maven/Gradle, los ficheros de recursos (config, plantillas, etc.) van en src/main/resources y se empaquetan dentro del jar. No se acceden con ruta absoluta del sistema, sino mediante el classpath.
Ejemplo 1 – Leer un recurso desde el classpath:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ResourceExample {
public static void main(String[] args) {
ClassLoader loader = ResourceExample.class.getClassLoader();
try (InputStream in = loader.getResourceAsStream("config/app.properties")) {
if (in == null) {
System.err.println("Recurso no encontrado.");
return;
}
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(in))) {
reader.lines().forEach(System.out::println);
}
} catch (IOException e) {
System.err.println("Error leyendo recurso: " + e.getMessage());
}
}
}
Ejemplo 2 – Cargar un JSON de recursos:
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class JsonResourceExample {
public static void main(String[] args) throws Exception {
ClassLoader loader = JsonResourceExample.class.getClassLoader();
try (InputStream in = loader.getResourceAsStream("data/users.json")) {
if (in == null) {
throw new IllegalStateException("users.json no encontrado");
}
String json = new String(in.readAllBytes(), StandardCharsets.UTF_8);
System.out.println(json);
}
}
}
Buen hábito
Cualquier fichero que tu aplicación necesite en producción debería ir en resources, no en rutas "hardcodeadas" de tu PC.
Preguntas / ejercicios
Question
- ¿Por qué no puedes usar directamente
Path.ofpara leer un recurso dentro deljar? - Explica cómo organizarías en
resourceslas plantillas HTML de una aplicación web. - ¿Qué ventajas tiene empaquetar recursos dentro del
jar? - Implementa un método que devuelva un
InputStreampara un recurso dado su nombre.
2.7 Ficheros temporales
Los ficheros temporales se usan como "papel borrador" para datos intermedios. Java permite crearlos de forma segura en el directorio temporal del sistema.
Ejemplo 1 – Crear un fichero temporal:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class TempFileExample {
public static void main(String[] args) {
try {
Path tempFile = Files.createTempFile("daw-", ".tmp");
System.out.println("Temporal: " + tempFile);
} catch (IOException e) {
System.err.println("No se pudo crear el temporal: " + e.getMessage());
}
}
}
Ejemplo 2 – Crear directorio temporal específico:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class TempDirectoryExample {
public static void main(String[] args) {
try {
Path tempDir = Files.createTempDirectory("daw-cache-");
System.out.println("Directorio temporal: " + tempDir);
} catch (IOException e) {
System.err.println("Error creando directorio temporal: " + e.getMessage());
}
}
}
Warning
El sistema no está obligado a borrar los temporales por ti. En un servidor, no limpiarlos es como nunca vaciar la papelera: al final te quedas sin espacio.
Preguntas / ejercicios
Question
- ¿En qué situaciones reales usarías ficheros temporales en una aplicación web?
- ¿Cómo te asegurarías de borrar los temporales al final de su uso?
- ¿Qué diferencias hay entre
createTempFileycreateTempDirectory? - Diseña un pequeño flujo donde proceses un CSV grande utilizando un temporal.
2.8 Ficheros comprimidos (ZIP)
Java incluye soporte para ZIP mediante java.util.zip, y en versiones recientes se han añadido métodos en Files para trabajar con sistemas de archivos ZIP.
Ejemplo 1 – Leer entradas de un ZIP:
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class ZipReadExample {
public static void main(String[] args) {
String zipPath = "data/archivos.zip";
try (ZipFile zipFile = new ZipFile(zipPath)) {
zipFile.stream()
.forEach(entry -> System.out.println("Entrada: " + entry.getName()));
} catch (IOException e) {
System.err.println("Error leyendo ZIP: " + e.getMessage());
}
}
}
Ejemplo 2 – Crear un ZIP simple:
import java.io.FileInputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class ZipWriteExample {
public static void main(String[] args) {
Path source = Path.of("data", "nombres.txt");
Path zipPath = Path.of("data", "backup.zip");
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipPath));
FileInputStream fis = new FileInputStream(source.toFile())) {
ZipEntry entry = new ZipEntry(source.getFileName().toString());
zos.putNextEntry(entry);
fis.transferTo(zos);
zos.closeEntry();
} catch (IOException e) {
System.err.println("Error creando ZIP: " + e.getMessage());
}
}
}
flowchart LR
A["FileInputStream<br/>nombres.txt"] --> B["ZipOutputStream<br/>backup.zip"]
B --> C["Entrada ZIP<br/>\"nombres.txt\""]
Preguntas / ejercicios
Question
- ¿Por qué puede ser útil comprimir logs antes de guardarlos a largo plazo?
- ¿Qué cuidados de seguridad tendrías al descomprimir ZIPs subidos por usuarios?
- Implementa un método que comprima todos los
.txtde un directorio en un único ZIP. - ¿Cómo listarías solo directorios dentro de un ZIP?
3. Flujos de Entrada y Salida con NIO (New I/O)
3.1 De java.io a java.nio: el salto generacional
Java nació con el paquete java.io para manejar toda la entrada/salida: FileInputStream, FileOutputStream, FileReader, FileWriter... Funcionaba, pero tenía limitaciones importantes: era bloqueante por diseño, bastante verboso (necesitabas encadenar muchas clases para una simple lectura) y no aprovechaba las capacidades modernas del sistema operativo.
Con Java 1.4 llegó java.nio (New I/O), y con Java 7 se reforzó con NIO.2 (java.nio.file), que es la API que usamos hoy en el desarrollo profesional. Piensa en la diferencia como pasar de un teléfono fijo (java.io) a un smartphone (java.nio): ambos sirven para comunicarte, pero el segundo te ofrece muchas más posibilidades, eficiencia y comodidad.
¿Por qué NIO? Estas son las razones clave:
| Aspecto | java.io (clásico) |
java.nio / NIO.2 (moderno) |
|---|---|---|
| Modelo de datos | Streams (flujo de bytes/chars) | Channels + Buffers (bloques de datos) |
| Lectura/escritura de texto | FileReader / FileWriter encadenados |
Files.readString(), Files.lines(), Files.newBufferedReader() |
| Lectura/escritura de bytes | FileInputStream / FileOutputStream |
Files.readAllBytes(), FileChannel + ByteBuffer |
| Acceso aleatorio | RandomAccessFile |
SeekableByteChannel, FileChannel |
| Operaciones de fichero | File.mkdir(), File.delete() |
Files.createDirectories(), Files.delete(), Files.copy() |
| Recorrido de directorios | File.listFiles() |
Files.list(), Files.walk(), Files.find() (devuelven Stream) |
| Verbosidad | Alta (muchas capas de decoradores) | Baja (métodos de conveniencia en Files) |
| E/S no bloqueante | No soportada | Soportada con Selector y canales asíncronos |
Idea clave
En código moderno, deberías usar java.nio.file (Path, Files, FileChannel) como primera opción. Solo recurres a java.io cuando una librería externa lo exija o para operaciones de bajo nivel muy específicas.
Ejemplo 1 – Comparación: leer texto con java.io vs java.nio:
=== "java.io (clásico)"
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadIoClassic {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(
new FileReader("data/mensaje.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
=== "java.nio (moderno)"
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadNioModern {
public static void main(String[] args) {
try {
String content = Files.readString(Path.of("data/mensaje.txt"));
System.out.println(content);
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
¿Ves la diferencia? Con NIO, una sola línea (Files.readString) hace lo mismo que cuatro con java.io. Menos código, menos errores, más legible.
Ejemplo 2 – Comparación: copiar fichero con java.io vs java.nio:
=== "java.io (clásico)"
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopyIoClassic {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream("data/origen.bin");
FileOutputStream out = new FileOutputStream("data/copia.bin")) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
=== "java.nio (moderno)"
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class CopyNioModern {
public static void main(String[] args) {
try {
Files.copy(
Path.of("data/origen.bin"),
Path.of("data/copia.bin"),
StandardCopyOption.REPLACE_EXISTING
);
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
flowchart TD
subgraph "java.io (clásico)"
A1["FileInputStream"] --> A2["byte[] buffer"]
A2 --> A3["FileOutputStream"]
end
subgraph "java.nio (moderno)"
B1["Files.copy(src, dst)"]
end
style B1 fill:#4CAF50,color:#fff
Tip
Files.copy no solo es más breve: internamente puede usar zero-copy del sistema operativo, lo que significa que los datos pasan directo del disco de origen al de destino sin pasar por la memoria de Java. Es como un "teleportador" de bytes.
Preguntas / ejercicios
Question
- ¿Qué problemas concretos del modelo
java.ioresuelvejava.nio? - ¿En qué situaciones seguirías necesitando
java.ioen un proyecto moderno? - Investiga qué es el "zero-copy" y por qué
Files.copypuede ser más rápido que copiar con buffer manual. - ¿Qué relación hay entre
Path,FilesyFileChannelen la API NIO?
3.2 Channels y Buffers: el nuevo paradigma
El corazón de NIO son dos conceptos: Channel (canal) y Buffer. Si los streams de java.io son tuberías por las que pasa el agua gota a gota, los canales de NIO son autopistas donde los datos viajan en bloques empaquetados en buffers.
Un Channel (java.nio.channels) es una conexión bidireccional a un recurso de E/S (fichero, socket, etc.). A diferencia de los streams que son unidireccionales (o lees o escribes), un FileChannel puede hacer ambas cosas.
Un Buffer (java.nio.ByteBuffer, CharBuffer, etc.) es un bloque de memoria donde se almacenan los datos antes de enviarlos al canal o después de recibirlos. Funciona como una "bandeja" intermedia.
flowchart LR
A["Código Java"] <-->|"put / get"| B["ByteBuffer<br/>(memoria)"]
B <-->|"read / write"| C["FileChannel<br/>(fichero en disco)"]
El ciclo de vida de un ByteBuffer:
- Escribir datos en el buffer:
buffer.put(data)ochannel.read(buffer) - Flip (voltear):
buffer.flip()— prepara el buffer para lectura, moviendo el límite a la posición actual y la posición a 0 - Leer datos del buffer:
buffer.get()ochannel.write(buffer) - Clear/Compact:
buffer.clear()para reutilizarlo, obuffer.compact()para conservar datos no leídos
Analogía
Un ByteBuffer es como una bandeja de sushi en un restaurante giratorio: primero el chef la llena (put), luego le da la vuelta para que tú la leas (flip), tú la vacías comiendo (get) y cuando acabas la devuelves al chef (clear).
Ejemplo 1 – Lectura de un fichero con FileChannel y ByteBuffer:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class ChannelReadExample {
public static void main(String[] args) {
Path path = Path.of("data", "mensaje.txt");
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
}
} catch (IOException e) {
System.err.println("Error leyendo con canal: " + e.getMessage());
}
}
}
Solo ASCII
Este ejemplo usa (char) buffer.get() para imprimir cada byte como carácter. Esto solo funciona con texto ASCII (letras sin tildes, sin emojis). Para texto UTF‑8 real, usa Files.readString o Files.newBufferedReader, que gestionan la codificación automáticamente.
Ejemplo 2 – Escritura con FileChannel y ByteBuffer:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class ChannelWriteExample {
public static void main(String[] args) {
Path path = Path.of("data", "salida_canal.txt");
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
String text = "Hola desde un FileChannel NIO\n";
ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
channel.write(buffer);
} catch (IOException e) {
System.err.println("Error escribiendo con canal: " + e.getMessage());
}
}
}
sequenceDiagram
participant C as Código
participant B as ByteBuffer
participant Ch as FileChannel
participant D as Disco
C->>B: wrap("Hola...")
C->>Ch: open(path, WRITE)
C->>Ch: write(buffer)
Ch->>D: datos al disco
C->>Ch: close()
Warning
No olvides llamar a flip() después de escribir en el buffer y antes de leerlo. Es el error más común de NIO y puede dejarte horas buscando por qué no se leen datos.
Tabla de StandardOpenOption más usadas:
| Opción | Descripción |
|---|---|
READ |
Abre para lectura |
WRITE |
Abre para escritura |
CREATE |
Crea el fichero si no existe |
CREATE_NEW |
Crea el fichero; falla si ya existe |
TRUNCATE_EXISTING |
Vacía el contenido si ya existe |
APPEND |
Escribe al final del fichero |
DELETE_ON_CLOSE |
Borra el fichero al cerrarlo |
Preguntas / ejercicios
Question
- ¿Qué ventaja tiene el modelo Channel+Buffer frente al modelo Stream de
java.io? - ¿Qué ocurre si olvidas llamar a
flip()antes de leer del buffer? - ¿Cuál es la diferencia entre
ByteBuffer.allocate()yByteBuffer.allocateDirect()? - Implementa una copia de fichero binario usando
FileChannelyByteBuffercon un buffer de 8 KB.
3.3 Copia eficiente con FileChannel.transferTo
FileChannel ofrece un método de copia de alto rendimiento que puede realizar zero-copy: transferTo. Esto permite que el sistema operativo mueva los datos directamente entre ficheros sin que pasen por la memoria de la JVM.
Ejemplo 1 – Copia eficiente fichero a fichero:
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class TransferToExample {
public static void main(String[] args) {
Path source = Path.of("data", "video.mp4");
Path target = Path.of("data", "video_backup.mp4");
try (FileChannel src = FileChannel.open(source, StandardOpenOption.READ);
FileChannel dst = FileChannel.open(target,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
src.transferTo(0, src.size(), dst);
System.out.println("Copia completada: " + Files.size(target) + " bytes");
} catch (IOException e) {
System.err.println("Error en transferencia: " + e.getMessage());
}
}
}
Ejemplo 2 – Copia segmentada para ficheros grandes (>2 GB):
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class LargeFileTransfer {
public static void main(String[] args) {
Path source = Path.of("data", "backup_grande.iso");
Path target = Path.of("data", "backup_grande_copy.iso");
try (FileChannel src = FileChannel.open(source, StandardOpenOption.READ);
FileChannel dst = FileChannel.open(target,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
long totalSize = src.size();
long transferred = 0;
while (transferred < totalSize) {
transferred += src.transferTo(transferred, totalSize - transferred, dst);
}
System.out.printf("Transferidos %d bytes%n", transferred);
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Tip
transferTo puede no transferir todos los bytes de una sola vez en ficheros muy grandes (>2 GB en algunos sistemas). Por eso el bucle del ejemplo 2 es la forma segura y profesional de usarlo.
Preguntas / ejercicios
Question
- ¿Qué significa "zero-copy" y por qué mejora el rendimiento?
- ¿Cuándo usarías
transferToen lugar deFiles.copy? - ¿Qué ventajas tiene
transferTofrente a copiar manualmente con unByteBuffer? - Implementa un método que copie solo los primeros N bytes de un fichero usando
transferTo.
3.4 Lectura y escritura de texto con NIO
Para ficheros de texto, NIO.2 ofrece métodos de conveniencia en Files que hacen innecesario encadenar FileReader, InputStreamReader, BufferedReader, etc. Son los que usarás en el 90% de los casos.
Método de Files |
Descripción | Ideal para |
|---|---|---|
readString(Path) |
Lee todo el fichero a un String |
Ficheros pequeños/medianos |
readAllLines(Path) |
Lee todas las líneas a List<String> |
Cuando necesitas la lista completa |
lines(Path) |
Devuelve un Stream<String> lazy |
Ficheros grandes, procesamiento funcional |
newBufferedReader(Path) |
Devuelve un BufferedReader |
Control manual línea a línea |
writeString(Path, String) |
Escribe un String completo |
Ficheros pequeños/medianos |
write(Path, Iterable) |
Escribe una colección de líneas | Listas de datos |
newBufferedWriter(Path) |
Devuelve un BufferedWriter |
Control manual, escritura incremental |
Ejemplo 1 – Lectura de todo el contenido con Files.readString:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class NioReadStringExample {
public static void main(String[] args) {
Path path = Path.of("data", "config.txt");
try {
String content = Files.readString(path, StandardCharsets.UTF_8);
System.out.println("Contenido:\n" + content);
} catch (IOException e) {
System.err.println("Error leyendo: " + e.getMessage());
}
}
}
Ejemplo 2 – Escritura de texto con Files.writeString y opciones:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class NioWriteStringExample {
public static void main(String[] args) {
Path path = Path.of("data", "log.txt");
String message = "2025-01-15 INFO: Servidor iniciado\n";
try {
Files.createDirectories(path.getParent());
Files.writeString(path, message, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
System.out.println("Línea añadida al log.");
} catch (IOException e) {
System.err.println("Error escribiendo: " + e.getMessage());
}
}
}
Info
Fíjate en StandardOpenOption.APPEND: sin esta opción, writeString sobrescribe el fichero cada vez. Con APPEND, los datos se añaden al final, perfecto para logs.
flowchart LR
subgraph "Lectura NIO"
A["Path"] --> B["Files.readString"]
B --> C["String"]
A --> D["Files.readAllLines"]
D --> E["List<String>"]
A --> F["Files.lines"]
F --> G["Stream<String>"]
end
Preguntas / ejercicios
Question
- ¿Cuándo usarías
readStringfrente alines? - ¿Qué pasa si usas
writeStringsinAPPENDsobre un fichero que ya tiene datos? - ¿Por qué
Files.linesdevuelve unStreamen vez de unaList? - Implementa un método que lea un fichero, cuente las palabras totales y las muestre por consola.
3.5 BufferedReader y BufferedWriter en NIO
Aunque Files.readString y Files.lines cubren la mayoría de casos, a veces necesitas control fino sobre la lectura o escritura. NIO ofrece Files.newBufferedReader y Files.newBufferedWriter para eso, sin necesidad de encadenar FileReader o FileWriter.
Ejemplo 1 – Lectura con Files.newBufferedReader:
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class NioBufferedReaderExample {
public static void main(String[] args) {
Path path = Path.of("data", "mensajes.txt");
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(">> " + line);
}
} catch (IOException e) {
System.err.println("Error leyendo: " + e.getMessage());
}
}
}
Ejemplo 2 – Escritura con Files.newBufferedWriter y opciones:
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class NioBufferedWriterExample {
public static void main(String[] args) {
Path path = Path.of("data", "report.txt");
try (BufferedWriter writer = Files.newBufferedWriter(path,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING)) {
writer.write("Informe de notas DAW");
writer.newLine();
writer.write("====================");
writer.newLine();
for (int i = 1; i <= 5; i++) {
writer.write(String.format("Alumno %d: %.1f%n", i, Math.random() * 10));
}
} catch (IOException e) {
System.err.println("Error escribiendo: " + e.getMessage());
}
}
}
Tip
La ventaja de Files.newBufferedReader/Writer sobre los constructores clásicos (new BufferedReader(new FileReader(...))) es que especificas la codificación directamente y usas Path en vez de String para la ruta. Más limpio, más seguro.
Preguntas / ejercicios
Question
- ¿Qué diferencia hay entre
Files.newBufferedReaderynew BufferedReader(new FileReader(...))? - ¿Cuándo necesitarías un
BufferedWriteren lugar deFiles.writeString? - ¿Qué pasa si no especificas codificación en
newBufferedReader? - Implementa un método que copie un fichero de texto línea a línea usando
newBufferedReaderynewBufferedWriter.
3.6 PrintWriter y Scanner con NIO
PrintWriter sigue siendo muy útil para escribir datos formateados (printf, println). Scanner sigue siendo cómodo para leer datos con parsing automático. Ambos se pueden combinar con la API NIO.
Ejemplo 1 – PrintWriter sobre Files.newBufferedWriter:
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class PrintWriterNioExample {
public static void main(String[] args) {
Path path = Path.of("data", "notas_report.txt");
try (PrintWriter pw = new PrintWriter(
Files.newBufferedWriter(path, StandardCharsets.UTF_8))) {
pw.println("Informe de notas");
pw.printf("Alumno: %s, Nota: %.2f%n", "Ana", 8.75);
pw.printf("Alumno: %s, Nota: %.2f%n", "Pepe", 6.50);
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Ejemplo 2 – Scanner con Path de NIO:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Scanner;
public class ScannerNioExample {
public static void main(String[] args) throws IOException {
Path path = Path.of("data", "numbers.txt");
try (Scanner scanner = new Scanner(path, StandardCharsets.UTF_8)) {
while (scanner.hasNextInt()) {
int value = scanner.nextInt();
System.out.println("Valor: " + value);
}
}
}
}
Info
Desde Java 10, Scanner acepta directamente un Path y un Charset en su constructor. Ya no hace falta pasar por new File(...).
Preguntas / ejercicios
Question
- ¿Qué diferencia hay entre
print,printlnyprintfenPrintWriter? - ¿Cuándo preferirías
Scannerfrente aFiles.lines()para leer un fichero? - Implementa una lectura de fichero CSV con
Scannerusando;como delimitador. - ¿Qué excepciones lanza
Scannersi el siguiente token no es del tipo esperado?
3.7 Flujos especiales: DataInputStream / DataOutputStream con NIO
Para leer y escribir tipos primitivos en formato binario (int, double, boolean, UTF-8 strings), Java usa DataInputStream y DataOutputStream. Estos siguen siendo de java.io, pero los combinamos con NIO para abrir los ficheros.
Ejemplo 1 – Escribir y leer primitivos combinando NIO con Data streams:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class DataStreamsNioExample {
public static void main(String[] args) {
Path path = Path.of("data", "alumno.dat");
try (DataOutputStream out = new DataOutputStream(
Files.newOutputStream(path))) {
out.writeUTF("Ana");
out.writeInt(1);
out.writeDouble(8.75);
} catch (IOException e) {
System.err.println("Error escribiendo: " + e.getMessage());
}
try (DataInputStream in = new DataInputStream(
Files.newInputStream(path))) {
String nombre = in.readUTF();
int conv = in.readInt();
double nota = in.readDouble();
System.out.printf("Alumno: %s, Conv: %d, Nota: %.2f%n", nombre, conv, nota);
} catch (IOException e) {
System.err.println("Error leyendo: " + e.getMessage());
}
}
}
Ejemplo 2 – Puentes NIO: Files.newInputStream y Files.newOutputStream:
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class NioBridgeExample {
public static void main(String[] args) {
Path path = Path.of("data", "raw.bin");
try (InputStream in = new BufferedInputStream(Files.newInputStream(path))) {
int value;
while ((value = in.read()) != -1) {
System.out.printf("0x%02X ", value);
}
System.out.println();
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
flowchart LR
A["Files.newOutputStream(path)"] --> B["DataOutputStream"]
B --> C["writeUTF / writeInt / writeDouble"]
D["Files.newInputStream(path)"] --> E["DataInputStream"]
E --> F["readUTF / readInt / readDouble"]
Danger
Cuando uses DataInputStream, debes leer en el mismo orden en que escribiste los datos, o el resultado será caos total. Es como leer una receta cambiando el orden de los ingredientes.
¿Por qué seguimos usando java.io aquí?
DataInputStream/DataOutputStream no tienen equivalente directo en NIO. Lo importante es que abrimos los ficheros con NIO (Files.newInputStream, Files.newOutputStream) y usamos Path en lugar de String. La capa de "transporte de datos" sigue siendo java.io.
Preguntas / ejercicios
Question
- ¿Qué ventajas tiene usar
Files.newOutputStream(path)frente anew FileOutputStream(path.toFile())? - Explica qué podría pasar si cambias el orden de lectura respecto al de escritura en
DataInputStream. - Implementa una función que lea múltiples registros secuenciales desde un fichero binario de primitivos.
- ¿Por qué no existe un
DataChannelen NIO? ¿Cómo se resuelve esto conByteBuffer?
4. Ficheros de Texto (enfoque NIO moderno)
4.1 Lectura de ficheros de texto
Con la API NIO.2, leer un fichero de texto es extremadamente sencillo. Tienes tres estrategias principales según el tamaño del fichero y lo que necesites hacer con su contenido.
Estrategia 1: Files.readString — Lee todo el contenido a un solo String. Ideal para ficheros pequeños (configuraciones, plantillas, mensajes).
Estrategia 2: Files.readAllLines — Lee todas las líneas a una List<String>. Útil cuando necesitas acceso por índice o la lista completa en memoria.
Estrategia 3: Files.lines — Devuelve un Stream<String> perezoso (lazy). Perfecto para ficheros grandes porque no carga todo en memoria de golpe.
Ejemplo 1 – Las tres estrategias en acción:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
public class TextReadStrategies {
public static void main(String[] args) throws IOException {
Path path = Path.of("data", "poema.txt");
// Estrategia 1: Todo a un String
String allContent = Files.readString(path, StandardCharsets.UTF_8);
System.out.println("Longitud total: " + allContent.length() + " chars");
// Estrategia 2: Lista de líneas
List<String> allLines = Files.readAllLines(path, StandardCharsets.UTF_8);
System.out.println("Total líneas: " + allLines.size());
System.out.println("Primera línea: " + allLines.get(0));
// Estrategia 3: Stream perezoso (ideal para ficheros grandes)
try (Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)) {
long nonEmpty = stream.filter(line -> !line.isBlank()).count();
System.out.println("Líneas no vacías: " + nonEmpty);
}
}
}
Ejemplo 2 – Leer y buscar patrones con Files.lines + Streams:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class SearchInFile {
public static void main(String[] args) {
Path logFile = Path.of("logs", "server.log");
try (Stream<String> lines = Files.lines(logFile)) {
lines.filter(line -> line.contains("ERROR"))
.map(String::trim)
.forEach(System.out::println);
} catch (IOException e) {
System.err.println("Error leyendo log: " + e.getMessage());
}
}
}
Warning
Files.lines() devuelve un Stream que está asociado a un recurso de E/S (el fichero abierto). Siempre debes usarlo dentro de un try-with-resources o cerrarlo manualmente, o dejarás el fichero abierto.
flowchart TD
A["¿Qué necesitas?"] -->|"Todo el texto"| B["Files.readString"]
A -->|"Lista de líneas"| C["Files.readAllLines"]
A -->|"Procesar con Stream"| D["Files.lines"]
B --> E["String"]
C --> F["List<String>"]
D --> G["Stream<String><br/>(lazy, eficiente)"]
Preguntas / ejercicios
Question
- ¿Por qué es peligroso usar
readAllLinescon un fichero de 10 GB? - ¿Qué diferencia hay entre un
Stream<String>lazy y unaList<String>cargada en memoria? - Implementa un buscador de texto que reciba un fichero y una palabra y devuelva las líneas que la contengan.
- ¿Qué codificación usa
Files.readStringsi no especificas ninguna?
4.2 Lectura línea a línea
Leer línea a línea permite procesar ficheros grandes sin cargar todo en memoria. Con NIO tienes dos opciones principales: Files.lines() (funcional) o Files.newBufferedReader() (imperativo).
Ejemplo 1 – Contar líneas no vacías con Files.lines:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class CountNonEmptyLines {
public static void main(String[] args) {
Path path = Path.of("data", "logs.txt");
try (Stream<String> lines = Files.lines(path)) {
long count = lines.filter(line -> !line.isBlank()).count();
System.out.println("Líneas no vacías: " + count);
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Ejemplo 2 – Procesar CSV simple línea a línea con NIO:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class SimpleCsvReader {
public static void main(String[] args) {
Path path = Path.of("data", "users.csv");
try (Stream<String> lines = Files.lines(path)) {
lines.skip(1) // saltar cabecera
.map(line -> line.split(","))
.filter(parts -> parts.length >= 2)
.forEach(parts ->
System.out.printf("Usuario: %s, Email: %s%n", parts[0], parts[1]));
} catch (IOException e) {
System.err.println("Error leyendo CSV: " + e.getMessage());
}
}
}
flowchart LR
A["Fichero CSV"] --> B["Files.lines()"]
B --> C["skip(1) cabecera"]
C --> D["map → split(',')"]
D --> E["filter válidos"]
E --> F["forEach imprimir"]
Tip
Fíjate en el skip(1): es la forma elegante y funcional de saltar la línea de cabecera de un CSV. Con BufferedReader tendrías que hacer un readLine() "vacío" antes del bucle.
Preguntas / ejercicios
Question
- ¿Por qué
Files.lineses más eficiente quereadAllLinespara ficheros grandes? - ¿Qué problemas tiene usar
split(",")en un CSV real con campos entrecomillados? - Implementa una función que devuelva solo las primeras N líneas de un fichero usando
Files.lines().limit(n). - ¿Cómo procesarías un fichero de 5 GB línea a línea sin quedarte sin memoria?
4.3 Escritura de ficheros de texto
NIO ofrece métodos concisos para escribir texto. La elección depende de si tienes el texto de golpe o lo generas poco a poco.
Ejemplo 1 – Files.writeString para texto completo:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class WriteStringExample {
public static void main(String[] args) {
Path path = Path.of("data", "saludo.txt");
String content = "Hola DAW\nEsto es un fichero de texto.\n";
try {
Files.writeString(path, content, StandardCharsets.UTF_8);
System.out.println("Fichero escrito.");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Ejemplo 2 – Files.write con lista de líneas:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class WriteLinesExample {
public static void main(String[] args) {
Path path = Path.of("data", "nombres.txt");
List<String> lines = List.of("Ana", "Luis", "María", "Carlos");
try {
Files.write(path, lines, StandardCharsets.UTF_8);
System.out.println("Escritas " + lines.size() + " líneas.");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Ejemplo 3 – Añadir texto al final (append) con StandardOpenOption:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
public class AppendLinesExample {
public static void main(String[] args) {
Path path = Path.of("data", "nombres.txt");
List<String> extras = List.of("Elena", "Pedro");
try {
Files.write(path, extras, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
System.out.println("Líneas añadidas al fichero.");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Info
La combinación Files.write + List<String> + StandardOpenOption.APPEND es extremadamente práctica para logs, registros de auditoría o cualquier dato que se acumula.
Preguntas / ejercicios
Question
- ¿Qué pasa si usas
Files.writeStringsinAPPENDsobre un fichero que ya tiene datos? - ¿Cómo escribirías texto usando una codificación diferente a UTF-8?
- Implementa un método que reciba una
List<String>y la escriba en un fichero, una palabra por línea. - ¿Cuándo elegirías
newBufferedWriterfrente aFiles.writeString?
4.4 Buenas prácticas en ficheros de texto
- Usar siempre UTF‑8 salvo motivo muy claro.
- Cerrar los recursos con
try-with-resources. - Separar claramente datos de logs.
- Usar
Files.lines()para ficheros grandes en lugar de cargar todo en memoria.
Warning
Modificar un fichero "in place" línea a línea puede ser peligroso; normalmente se escribe a un temporal y al final se sustituye el original con Files.move.
Ejemplo 1 – Uso de temporal para modificar un texto (patrón profesional):
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ReplaceWordInFile {
public static void main(String[] args) throws IOException {
Path original = Path.of("data", "texto.txt");
Path temp = Files.createTempFile("texto-", ".tmp");
try (Stream<String> lines = Files.lines(original, StandardCharsets.UTF_8)) {
var modified = lines
.map(line -> line.replace("ERROR", "INFO"))
.collect(Collectors.toList());
Files.write(temp, modified, StandardCharsets.UTF_8);
}
Files.move(temp, original, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Fichero modificado correctamente.");
}
}
Ejemplo 2 – Log y datos separados (con NIO):
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class LogAndDataExample {
public static void main(String[] args) {
Path dataPath = Path.of("data", "datos.txt");
Path logPath = Path.of("logs", "app.log");
try {
Files.createDirectories(dataPath.getParent());
Files.createDirectories(logPath.getParent());
try (PrintWriter dataWriter = new PrintWriter(
Files.newBufferedWriter(dataPath, StandardCharsets.UTF_8));
PrintWriter logWriter = new PrintWriter(
Files.newBufferedWriter(logPath, StandardCharsets.UTF_8))) {
dataWriter.println("Dato importante 1");
logWriter.println("INFO: Dato 1 escrito correctamente");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
graph TD
A["Servicio"] --> B["datos.txt"]
A --> C["app.log"]
Preguntas / ejercicios
Question
- ¿Por qué es buena idea usar un fichero temporal para modificaciones?
- ¿Qué estrategias de backup aplicarías a ficheros críticos de configuración?
- ¿Cómo gestionarías logs rotativos (log diario) con Java?
- Relaciona estas prácticas con el despliegue de aplicaciones en producción.
5. Ficheros Binarios (enfoque NIO moderno)
5.1 Concepto de fichero binario
Un fichero binario guarda datos en un formato compacto, normalmente no legible por humanos, y pensado para ser leído por programas que conocen su estructura interna. Son ideales para rendimiento y espacio.
Info
Piensa en una partida guardada de un juego: nadie la edita a mano, pero el juego la entiende perfectamente. Un fichero .png, un .mp3 o un .class compilado son todos ficheros binarios.
Preguntas / ejercicios
Question
- ¿Qué ventaja clara ofrece un fichero binario sobre uno de texto para guardar muchos enteros?
- ¿Qué pasa si intentas abrir un binario con un editor de texto y lo modificas?
- ¿Por qué es tan importante documentar la estructura de un fichero binario?
- Propón un formato binario simple para guardar una lista de puntuaciones de juego.
5.2 Lectura de ficheros binarios
Con NIO, tienes dos enfoques principales para leer binarios:
Files.readAllBytes: lee todo el fichero en unbyte[]. Simple y directo para ficheros que caben en memoria.FileChannel+ByteBuffer: control total sobre la lectura, ideal para ficheros grandes o cuando necesitas leer bloques específicos.
Ejemplo 1 – Lectura completa con Files.readAllBytes:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class BinaryReadAll {
public static void main(String[] args) {
Path path = Path.of("data", "raw.bin");
try {
byte[] data = Files.readAllBytes(path);
System.out.println("Tamaño: " + data.length + " bytes");
for (int i = 0; i < Math.min(data.length, 20); i++) {
System.out.printf("0x%02X ", data[i]);
}
System.out.println();
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Ejemplo 2 – Lectura con FileChannel y ByteBuffer (ficheros grandes):
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class BinaryReadChannel {
public static void main(String[] args) {
Path path = Path.of("data", "video.mp4");
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(8 * 1024);
long totalBytes = 0;
while (channel.read(buffer) != -1) {
buffer.flip();
totalBytes += buffer.remaining();
buffer.clear();
}
System.out.printf("Leídos %d bytes del fichero%n", totalBytes);
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
flowchart TD
A["¿Cabe en memoria?"] -->|"Sí"| B["Files.readAllBytes"]
A -->|"No / Fichero grande"| C["FileChannel + ByteBuffer"]
B --> D["byte[]"]
C --> E["Procesar por bloques"]
Preguntas / ejercicios
Question
- ¿Cuándo usarías
readAllBytesy cuándoFileChannel? - ¿Qué sucede si intentas
readAllBytescon un fichero de 4 GB? - ¿Cómo detectarías si la estructura del fichero binario es la esperada (por ejemplo, leyendo un "magic number")?
- Implementa un método que muestre los primeros N bytes de cualquier fichero en formato hexadecimal.
5.3 Escritura de ficheros binarios
Para escribir binarios con NIO usamos Files.write(Path, byte[]) para datos pequeños o FileChannel + ByteBuffer para más control.
Ejemplo 1 – Escritura simple con Files.write:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class BinaryWriteSimple {
public static void main(String[] args) {
byte[] data = {0x48, 0x6F, 0x6C, 0x61, 0x0A}; // "Hola\n" en ASCII
Path path = Path.of("data", "saludo.bin");
try {
Files.write(path, data);
System.out.println("Escritos " + data.length + " bytes.");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Ejemplo 2 – Escritura con FileChannel y ByteBuffer:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class BinaryWriteChannel {
public static void main(String[] args) {
Path path = Path.of("data", "scores.bin");
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
int[] scores = {1500, 3200, 750, 9999, 4100};
ByteBuffer buffer = ByteBuffer.allocate(scores.length * Integer.BYTES);
for (int score : scores) {
buffer.putInt(score);
}
buffer.flip();
channel.write(buffer);
System.out.println("Puntuaciones guardadas.");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Warning
Fíjate en el buffer.flip() antes de channel.write(): sin él, el canal intenta leer desde la posición actual del buffer (que está al final) y no escribe nada. Es el clásico "bug fantasma" de NIO.
Preguntas / ejercicios
Question
- ¿Qué diferencia hay entre
Files.write(path, bytes)y usarFileChannel? - ¿Por qué usamos
Integer.BYTESpara calcular el tamaño del buffer? - Diseña un formato binario para almacenar registros compactos de cliente (nombre fijo 20 chars + edad int + saldo double).
- ¿Qué implicaciones tiene el orden de bytes (endianness) en ficheros binarios portables?
5.4 Operaciones avanzadas con ByteBuffer
ByteBuffer es la pieza central de NIO para datos binarios. Ofrece métodos tipados (putInt, getDouble, putFloat...) que permiten trabajar con tipos primitivos directamente sobre el buffer, sin necesidad de DataInputStream/DataOutputStream.
Ejemplo 1 – Escribir y leer un registro estructurado con ByteBuffer:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class ByteBufferRecordExample {
private static final int NAME_LENGTH = 20;
public static void main(String[] args) throws IOException {
Path path = Path.of("data", "alumno_nio.dat");
// Escribir registro
try (FileChannel ch = FileChannel.open(path,
StandardOpenOption.CREATE, StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
ByteBuffer buf = ByteBuffer.allocate(NAME_LENGTH + Integer.BYTES + Double.BYTES);
writeFixedString(buf, "Ana García", NAME_LENGTH);
buf.putInt(1);
buf.putDouble(9.25);
buf.flip();
ch.write(buf);
}
// Leer registro
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(NAME_LENGTH + Integer.BYTES + Double.BYTES);
ch.read(buf);
buf.flip();
String name = readFixedString(buf, NAME_LENGTH);
int conv = buf.getInt();
double grade = buf.getDouble();
System.out.printf("Alumno: %s, Conv: %d, Nota: %.2f%n", name, conv, grade);
}
}
private static void writeFixedString(ByteBuffer buf, String text, int length) {
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
buf.put(bytes, 0, Math.min(bytes.length, length));
for (int i = bytes.length; i < length; i++) {
buf.put((byte) 0);
}
}
private static String readFixedString(ByteBuffer buf, int length) {
byte[] bytes = new byte[length];
buf.get(bytes);
return new String(bytes, StandardCharsets.UTF_8).trim();
}
}
Ejemplo 2 – Transformar bytes (sumar 1 a cada byte):
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class IncrementBytesNio {
public static void main(String[] args) {
Path input = Path.of("data", "raw.bin");
Path output = Path.of("data", "raw_inc.bin");
try {
byte[] data = Files.readAllBytes(input);
ByteBuffer buffer = ByteBuffer.allocate(data.length);
for (byte b : data) {
buffer.put((byte) ((b + 1) & 0xFF));
}
buffer.flip();
try (FileChannel ch = FileChannel.open(output,
StandardOpenOption.CREATE, StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
ch.write(buffer);
}
System.out.println("Bytes transformados y guardados.");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
flowchart LR
A["ByteBuffer"] --> B["putInt / putDouble<br/>putFloat / put"]
B --> C["flip()"]
C --> D["getInt / getDouble<br/>getFloat / get"]
D --> E["clear() o compact()"]
E --> A
Métodos clave de ByteBuffer
allocate(capacity): crea un buffer en el heap de JavaallocateDirect(capacity): crea un buffer fuera del heap, más rápido para E/S pero más costoso de crearwrap(byte[]): envuelve un array existenteflip(): prepara para lectura después de escribirclear(): reinicia posición y límite para reutilizarcompact(): mueve los datos no leídos al principio
Preguntas / ejercicios
Question
- ¿Qué diferencia hay entre
ByteBuffer.allocateyByteBuffer.allocateDirect? - ¿Cuándo es mejor usar
ByteBufferfrente aDataOutputStreampara datos binarios? - ¿Qué pasa si sumas 1 a
0xFFsin hacer& 0xFF? - Implementa funciones de escritura/lectura de un registro "Producto" (nombre fijo, precio double, stock int) usando
ByteBuffer.
6. Ficheros de Acceso Aleatorio (enfoque NIO)
6.1 ¿Qué es el acceso aleatorio?
El acceso aleatorio permite saltar directamente a una posición específica del fichero, lo que es útil para estructuras con registros de tamaño fijo. Es como poder ir a la página 200 de un libro sin leer las 199 anteriores.
En java.io se usaba RandomAccessFile. En NIO, la interfaz SeekableByteChannel y la clase FileChannel ofrecen la misma capacidad pero integrada con el ecosistema NIO de Path, ByteBuffer y StandardOpenOption.
flowchart LR
A["Registro 0<br/>pos 0"] --> B["Registro 1<br/>pos REG_SIZE"]
B --> C["Registro 2<br/>pos 2×REG_SIZE"]
C --> D["Registro N<br/>pos N×REG_SIZE"]
E["seek(position)"] -.->|"salto directo"| B
Preguntas / ejercicios
Question
- ¿Qué ventaja tiene el acceso aleatorio frente al secuencial en grandes ficheros de registros?
- ¿Por qué suele requerir registros de tamaño fijo?
- Pon un ejemplo de aplicación real que se beneficie de acceso aleatorio.
- ¿Qué problemas podría causar un cambio en el tamaño de los registros?
6.2 SeekableByteChannel y FileChannel
SeekableByteChannel es la interfaz NIO que permite posicionarte en cualquier punto del fichero. FileChannel la implementa y añade funcionalidades extra (transferencia directa, bloqueo de ficheros, memory-mapped I/O).
Para obtener un SeekableByteChannel usamos Files.newByteChannel(path, options...). Para obtener un FileChannel usamos FileChannel.open(path, options...).
Ejemplo 1 – Escritura de registros de tamaño fijo con FileChannel:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class RandomAccessWriteNio {
private static final int NAME_SIZE = 20;
private static final int RECORD_SIZE = NAME_SIZE + Integer.BYTES; // 24 bytes
public static void main(String[] args) throws IOException {
Path path = Path.of("data", "registros_nio.dat");
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.READ,
StandardOpenOption.WRITE)) {
writeRecord(channel, 0, "Ana", 101);
writeRecord(channel, 1, "Pepe", 102);
writeRecord(channel, 2, "María", 103);
System.out.println("Registros escritos.");
}
}
private static void writeRecord(FileChannel channel, int index, String name, int id)
throws IOException {
long position = (long) index * RECORD_SIZE;
channel.position(position);
ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
buffer.put(nameBytes, 0, Math.min(nameBytes.length, NAME_SIZE));
for (int i = nameBytes.length; i < NAME_SIZE; i++) {
buffer.put((byte) 0);
}
buffer.putInt(id);
buffer.flip();
channel.write(buffer);
}
}
Ejemplo 2 – Lectura de un registro concreto por índice:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class RandomAccessReadNio {
private static final int NAME_SIZE = 20;
private static final int RECORD_SIZE = NAME_SIZE + Integer.BYTES;
public static void main(String[] args) throws IOException {
Path path = Path.of("data", "registros_nio.dat");
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
for (int i = 0; i < 3; i++) {
readRecord(channel, i);
}
// Acceso directo al registro 1 (sin leer los anteriores)
System.out.println("\n--- Acceso directo al registro 1 ---");
readRecord(channel, 1);
}
}
private static void readRecord(FileChannel channel, int index) throws IOException {
long position = (long) index * RECORD_SIZE;
channel.position(position);
ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
buffer.flip();
byte[] nameBytes = new byte[NAME_SIZE];
buffer.get(nameBytes);
String name = new String(nameBytes, StandardCharsets.UTF_8).trim();
int id = buffer.getInt();
System.out.printf("Registro %d → Nombre: %s, ID: %d%n", index, name, id);
}
}
Ejemplo 3 – Uso de SeekableByteChannel (interfaz genérica):
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class SeekableChannelExample {
public static void main(String[] args) throws IOException {
Path path = Path.of("data", "registros_nio.dat");
try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
long fileSize = channel.size();
System.out.println("Tamaño del fichero: " + fileSize + " bytes");
// Saltar al byte 24 (segundo registro)
channel.position(24);
ByteBuffer buffer = ByteBuffer.allocate(24);
channel.read(buffer);
buffer.flip();
byte[] nameBytes = new byte[20];
buffer.get(nameBytes);
String name = new String(nameBytes, StandardCharsets.UTF_8).trim();
int id = buffer.getInt();
System.out.printf("Registro en pos 24: %s (ID: %d)%n", name, id);
}
}
}
Warning
Cambiar el tamaño de un registro (por ejemplo añadiendo un campo nuevo) rompe los cálculos de position(). Es como cambiar el número de columnas de una tabla sin actualizar el código que la recorre. Siempre documenta el formato de tus registros.
¿FileChannel o SeekableByteChannel?
SeekableByteChannel es la interfaz general. FileChannel es la implementación concreta que añade extras como transferTo, lock() o map() (memory-mapped files). Usa SeekableByteChannel si solo necesitas position(), read() y write(). Usa FileChannel si necesitas las funcionalidades avanzadas.
Preguntas / ejercicios
Question
- ¿Qué diferencia hay entre
channel.position(n)y elseek(n)deRandomAccessFile? - ¿Cómo calcularías la posición de un registro dado su índice y tamaño?
- Diseña la estructura de un registro fijo para un sistema de reservas de hotel.
- ¿Qué problemas tendrías al eliminar registros en un fichero de acceso aleatorio? ¿Cómo los resolverías?
7. Serialización de Objetos
7.1 Concepto de serialización
La serialización convierte un objeto Java en bytes para almacenarlo o enviarlo. La deserialización reconstruye el objeto a partir de esos bytes.
Danger
La serialización nativa de Java tiene riesgos de seguridad; solo se debe usar con datos de fuentes confiables y considerar alternativas como JSON para intercambio entre sistemas.
Preguntas / ejercicios
Question
- ¿Qué ventajas tiene serializar objetos directamente frente a usar DTOs + JSON?
- ¿Qué riesgos de seguridad conoces asociados a la deserialización?
- ¿Por qué no es buena idea deserializar datos que vienen de clientes sin control?
- Piensa en un dominio de DAW donde te pueda interesar persistir objetos directamente.
7.2 Interfaz Serializable
Para permitir que una clase se serialice, debe implementar java.io.Serializable. Es una interfaz de marcador, sin métodos.
Ejemplo 1 – Clase simple serializable:
import java.io.Serializable;
public class Alumno implements Serializable {
private static final long serialVersionUID = 1L;
private String nombre;
private String email;
private double nota;
public Alumno(String nombre, String email, double nota) {
this.nombre = nombre;
this.email = email;
this.nota = nota;
}
@Override
public String toString() {
return String.format("Alumno{nombre='%s', email='%s', nota=%.2f}", nombre, email, nota);
}
// getters, setters...
}
Ejemplo 2 – Campo transient para excluir de la serialización:
import java.io.Serializable;
public class Usuario implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String passwordHash; // no se serializa
public Usuario(String username, String passwordHash) {
this.username = username;
this.passwordHash = passwordHash;
}
}
Info
serialVersionUID ayuda a controlar la compatibilidad entre versiones de la clase; si cambias la clase y no actualizas esto, puedes tener errores de deserialización.
Preguntas / ejercicios
Question
- ¿Por qué
Serializableno define métodos? - ¿Qué utilidad tiene la palabra clave
transient? - ¿Qué pasa si cambias la estructura de la clase sin actualizar
serialVersionUID? - ¿Qué datos nunca deberías serializar en claro?
7.3 ObjectInputStream / ObjectOutputStream con NIO
Estas clases permiten escribir y leer objetos completos, siempre que implementen Serializable. En el enfoque moderno, abrimos los ficheros usando Files.newOutputStream y Files.newInputStream en lugar de los clásicos FileOutputStream / FileInputStream.
Ejemplo 1 – Serializar un objeto (con NIO):
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class SerializeAlumno {
public static void main(String[] args) {
Alumno alumno = new Alumno("Ana", "ana@daw.es", 9.1);
Path path = Path.of("data", "alumno.obj");
try (ObjectOutputStream out = new ObjectOutputStream(
Files.newOutputStream(path))) {
out.writeObject(alumno);
System.out.println("Objeto serializado en " + path);
} catch (IOException e) {
System.err.println("Error serializando: " + e.getMessage());
}
}
}
Ejemplo 2 – Deserializar un objeto (con NIO):
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class DeserializeAlumno {
public static void main(String[] args) {
Path path = Path.of("data", "alumno.obj");
try (ObjectInputStream in = new ObjectInputStream(
Files.newInputStream(path))) {
Alumno alumno = (Alumno) in.readObject();
System.out.println("Alumno: " + alumno);
} catch (IOException | ClassNotFoundException e) {
System.err.println("Error deserializando: " + e.getMessage());
}
}
}
flowchart LR
A["Objeto Alumno"] --> B["ObjectOutputStream<br/>writeObject"]
B --> C["Fichero alumno.obj"]
C --> D["ObjectInputStream<br/>readObject"]
D --> E["Objeto Alumno"]
Preguntas / ejercicios
Question
- ¿Qué ocurre si intentas serializar una clase que no implementa
Serializable? - ¿Qué excepción puede lanzar
readObjectademás deIOException? - ¿Cómo se comporta la serialización con relaciones entre objetos (por ejemplo, listas)?
- Diseña un escenario donde guardes una lista de alumnos en un fichero de objetos.
7.4 Escritura de objetos serializados
Se pueden escribir múltiples objetos en el mismo fichero, pero hay que tener cuidado con abrir/cerrar streams y cabeceras.
Ejemplo 1 – Escritura de una lista de objetos:
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class SerializeList {
public static void main(String[] args) {
List<Alumno> alumnos = List.of(
new Alumno("Ana", "ana@daw.es", 9.1),
new Alumno("Pepe", "pepe@daw.es", 7.5)
);
Path path = Path.of("data", "alumnos.obj");
try (ObjectOutputStream out = new ObjectOutputStream(
Files.newOutputStream(path))) {
out.writeObject(alumnos);
System.out.println("Lista serializada.");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Ejemplo 2 – Varios objetos secuenciales:
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class SerializeMultiple {
public static void main(String[] args) {
Path path = Path.of("data", "multi_alumnos.obj");
try (ObjectOutputStream out = new ObjectOutputStream(
Files.newOutputStream(path))) {
out.writeObject(new Alumno("Ana", "ana@daw.es", 9.1));
out.writeObject(new Alumno("Pepe", "pepe@daw.es", 7.5));
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Warning
Si abres repetidamente un ObjectOutputStream sobre el mismo fichero en modo append, la cabecera se escribe varias veces y puedes tener problemas al leer. La forma segura es serializar una colección (List) en una sola operación.
Preguntas / ejercicios
Question
- ¿Qué ventajas tiene serializar una lista completa frente a serializar elementos sueltos?
- ¿Cómo leerías todos los objetos de un fichero hasta final de stream?
- ¿Por qué es problemático escribir múltiples cabeceras de
ObjectOutputStream? - ¿Qué alternativas a la serialización nativa usarías para interoperar con otros lenguajes?
7.5 Lectura de objetos serializados
Se usa readObject y se castea al tipo esperado, gestionando EOFException para detectar fin de fichero.
Ejemplo 1 – Leer una lista de golpe:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class DeserializeList {
public static void main(String[] args) {
Path path = Path.of("data", "alumnos.obj");
try (ObjectInputStream in = new ObjectInputStream(
Files.newInputStream(path))) {
@SuppressWarnings("unchecked")
List<Alumno> alumnos = (List<Alumno>) in.readObject();
alumnos.forEach(System.out::println);
} catch (IOException | ClassNotFoundException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Ejemplo 2 – Leer múltiples objetos hasta EOF:
import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class DeserializeMultiple {
public static void main(String[] args) {
Path path = Path.of("data", "multi_alumnos.obj");
try (ObjectInputStream in = new ObjectInputStream(
Files.newInputStream(path))) {
while (true) {
Alumno alumno = (Alumno) in.readObject();
System.out.println(alumno);
}
} catch (EOFException e) {
System.out.println("Fin de fichero.");
} catch (IOException | ClassNotFoundException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Preguntas / ejercicios
Question
- ¿Cómo detectas el final del fichero cuando lees objetos secuenciales?
- ¿Qué problemas puede causar cambiar la versión de la clase entre serialización y deserialización?
- Diseña una función que devuelva un
List<Alumno>desde un fichero de objetos. - ¿Cómo gestionarías errores de lectura parciales (fichero corrupto)?
7.6 Problemas comunes de serialización
- Cambios en la clase (añadir/quitar campos).
- Versiones diferentes de
serialVersionUID. - Serializar objetos no confiables.
Warning
Deserializar datos de origen desconocido puede permitir ataques de ejecución de código o DOS. En entornos modernos se recomiendan filtros de deserialización y formatos alternativos como JSON.
Preguntas / ejercicios
Question
- ¿Cómo ayuda
serialVersionUIDa manejar cambios de versión de una clase? - ¿Qué políticas de seguridad aplicarías en un servidor que recibe objetos serializados?
- ¿Por qué muchas aplicaciones modernas prefieren JSON o XML a la serialización nativa?
- Investiga la API de filtros de deserialización de Java y resume cómo podrías usarla.
8. Formatos de Intercambio de Datos
8.1 DTO (Data Transfer Object)
Los DTO se usan para transportar datos entre capas o servicios, a menudo en formatos como JSON, CSV o XML.
Ejemplo 1 – DTO simple:
public class UserDto {
private String username;
private String email;
private int age;
public UserDto() {
}
public UserDto(String username, String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}
public String getUsername() { return username; }
public String getEmail() { return email; }
public int getAge() { return age; }
@Override
public String toString() {
return String.format("UserDto{username='%s', email='%s', age=%d}", username, email, age);
}
}
Ejemplo 2 – Uso de DTO en una capa de servicio:
import java.util.List;
public class UserService {
public List<UserDto> getUsers() {
return List.of(
new UserDto("ana", "ana@daw.es", 20),
new UserDto("pepe", "pepe@daw.es", 22)
);
}
}
Info
Un DTO es como un "sobre estándar" para intercambiar datos entre microservicios o entre backend y frontend. No lleva lógica de negocio, solo datos.
Preguntas / ejercicios
Question
- ¿Por qué es buena idea separar DTOs de entidades de dominio?
- ¿Qué problemas se evitan usando DTOs en lugar de serializar entidades directamente?
- Diseña un DTO para un pedido en una tienda online.
- ¿Cómo mapearías entidades de BD a DTOs en una aplicación Java?
8.2 Ficheros properties (con NIO)
Los .properties guardan pares clave‑valor para configuración. Java tiene soporte nativo con la clase Properties. En el enfoque moderno, usamos NIO para abrir los ficheros.
Ejemplo 1 – Leer un fichero properties (desde classpath):
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class PropertiesReadExample {
public static void main(String[] args) {
Properties props = new Properties();
try (InputStream in =
PropertiesReadExample.class.getClassLoader()
.getResourceAsStream("config/app.properties")) {
if (in == null) {
throw new IllegalStateException("app.properties no encontrado");
}
props.load(in);
System.out.println("URL BD: " + props.getProperty("db.url"));
} catch (IOException e) {
System.err.println("Error properties: " + e.getMessage());
}
}
}
Ejemplo 2 – Escribir properties con NIO:
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
public class PropertiesWriteExample {
public static void main(String[] args) {
Properties props = new Properties();
props.setProperty("app.name", "MiApp");
props.setProperty("app.version", "1.0.0");
Path path = Path.of("config", "app.properties");
try {
Files.createDirectories(path.getParent());
try (OutputStream out = Files.newOutputStream(path)) {
props.store(out, "Configuración de la aplicación");
}
} catch (IOException e) {
System.err.println("Error guardando properties: " + e.getMessage());
}
}
}
Ejemplo 3 – Leer properties desde el sistema de ficheros con NIO:
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
public class PropertiesReadNioExample {
public static void main(String[] args) {
Path path = Path.of("config", "app.properties");
Properties props = new Properties();
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
props.load(reader);
props.forEach((key, value) ->
System.out.printf("%s = %s%n", key, value));
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
flowchart LR
A["Properties"] --> B["store()"]
B --> C["config/app.properties"]
C --> D["load()"]
D --> A
Preguntas / ejercicios
Question
- ¿Por qué es útil separar configuración de código usando
properties? - ¿Cómo gestionarías diferentes ficheros de properties para desarrollo y producción?
- Diseña un
app.propertiesmínimo para una aplicación web. - ¿Qué diferencia hay entre
getPropertyygetenProperties?
8.3 CSV (con NIO)
Los ficheros CSV son textos donde los campos se separan por comas (u otros delimitadores). Son muy usados para import/export de datos. Con NIO, escribir y leer CSV es más limpio usando Files.newBufferedWriter, Files.lines y Files.write.
Ejemplo 1 – Escribir CSV con NIO:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class CsvWriteNioExample {
public static void main(String[] args) {
List<UserDto> users = List.of(
new UserDto("ana", "ana@daw.es", 20),
new UserDto("pepe", "pepe@daw.es", 22)
);
List<String> lines = new ArrayList<>();
lines.add("username,email,age");
for (UserDto user : users) {
lines.add(user.getUsername() + "," + user.getEmail() + "," + user.getAge());
}
Path path = Path.of("data", "users.csv");
try {
Files.write(path, lines, StandardCharsets.UTF_8);
System.out.println("CSV escrito con " + users.size() + " registros.");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Ejemplo 2 – Leer CSV a DTOs con Files.lines y Streams:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CsvReadNioExample {
public static List<UserDto> readUsers(Path path) throws IOException {
try (Stream<String> lines = Files.lines(path)) {
return lines.skip(1) // saltar cabecera
.map(line -> line.split(","))
.filter(parts -> parts.length >= 3)
.map(parts -> new UserDto(
parts[0].trim(),
parts[1].trim(),
Integer.parseInt(parts[2].trim())))
.collect(Collectors.toList());
}
}
public static void main(String[] args) {
try {
List<UserDto> users = readUsers(Path.of("data", "users.csv"));
users.forEach(System.out::println);
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
flowchart LR
A["users.csv"] --> B["Files.lines()"]
B --> C["skip(1)"]
C --> D["map → split(',')"]
D --> E["map → UserDto"]
E --> F["collect → List"]
Warning
CSV real puede ser muy complejo (comillas, comas dentro de campos, saltos de línea). Para producción es mejor usar librerías específicas (OpenCSV, Apache Commons CSV, etc.).
Tip
Fíjate cómo el ejemplo 2 combina NIO (Files.lines) con Streams funcionales (skip, map, filter, collect). Este patrón es el pan de cada día en backend moderno: leer un fichero, transformar datos y recogerlos en una colección. Así trabajan los profesionales.
Preguntas / ejercicios
Question
- ¿Qué problemas tiene el ejemplo "naive" de parseo de CSV con
split(",")? - ¿Qué mejoras harías al escribir CSV (por ejemplo, escapado de comillas)?
- Diseña un CSV de pedidos de tienda online con cabecera y dos registros.
- ¿En qué casos preferirías JSON sobre CSV?
8.4 JSON
JSON es el formato estrella para APIs modernas y aplicaciones web. Java no tiene soporte JSON estándar en la plataforma base, pero se suele usar Jackson, Gson, etc.
Lectura de JSON
Ejemplo 1 – Leer JSON a DTO con Jackson (concepto):
// build.gradle o pom.xml incluiría la dependencia de Jackson
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Path;
public class JsonReadExample {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Path path = Path.of("data/user.json");
UserDto user = mapper.readValue(path.toFile(), UserDto.class);
System.out.println(user);
}
}
Objeto a JSON
Ejemplo 2 – Convertir DTO a JSON y guardarlo:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Path;
public class JsonWriteExample {
public static void main(String[] args) throws IOException {
UserDto user = new UserDto("ana", "ana@daw.es", 20);
ObjectMapper mapper = new ObjectMapper();
Path path = Path.of("data/user.json");
mapper.writeValue(path.toFile(), user);
}
}
JSON a Objeto y Pretty Print
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
public class JsonPrettyPrintExample {
public static void main(String[] args) throws Exception {
UserDto user = new UserDto("pepe", "pepe@daw.es", 22);
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String jsonPretty = mapper.writeValueAsString(user);
System.out.println(jsonPretty);
}
}
flowchart LR
A["UserDto"] --> B["ObjectMapper.writeValue"]
B --> C["user.json"]
C --> D["ObjectMapper.readValue"]
D --> E["UserDto"]
Info
JSON es el idioma común entre front y back. Es el "inglés" de las APIs REST.
Preguntas / ejercicios
Question
- ¿Qué ventajas tiene JSON frente a XML y CSV?
- ¿Por qué es cómodo usar librerías como Jackson en lugar de manejar JSON a mano?
- Diseña la estructura JSON para un usuario con dirección anidada.
- ¿Qué cuidados de seguridad debes tener al procesar JSON entrante?
8.5 XML
XML fue estándar durante años para configuración y comunicación, y sigue presente en muchos sistemas.
Objeto a XML
Ejemplo 1 – Usando JAXB (concepto):
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import java.io.File;
@XmlRootElement(name = "user")
class UserXml {
private String username;
private String email;
@XmlElement
public String getUsername() {
return username;
}
@XmlElement
public String getEmail() {
return email;
}
public UserXml() {
}
public UserXml(String username, String email) {
this.username = username;
this.email = email;
}
}
public class XmlWriteExample {
public static void main(String[] args) throws Exception {
UserXml user = new UserXml("ana", "ana@daw.es");
JAXBContext context = JAXBContext.newInstance(UserXml.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(user, new File("data/user.xml"));
}
}
XML a Objeto
Ejemplo 2 – Unmarshal XML a objeto:
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.Unmarshaller;
import java.io.File;
public class XmlReadExample {
public static void main(String[] args) throws Exception {
JAXBContext context = JAXBContext.newInstance(UserXml.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
UserXml user = (UserXml) unmarshaller.unmarshal(new File("data/user.xml"));
System.out.println(user.getUsername() + " - " + user.getEmail());
}
}
flowchart LR
A["UserXml"] --> B["Marshaller.marshal"]
B --> C["user.xml"]
C --> D["Unmarshaller.unmarshal"]
D --> E["UserXml"]
Tip
En proyectos nuevos suele preferirse JSON, pero el conocimiento de XML sigue siendo útil en integraciones con sistemas legados.
Preguntas / ejercicios
Question
- ¿Qué ventajas ofrece XML sobre JSON en ciertos contextos (por ejemplo, validación con XSD)?
- ¿Qué te parece más legible, JSON o XML? Justifica.
- Diseña un pequeño XML para representar una lista de productos.
- ¿Cómo relacionarías JAXB con los DTO que ya tienes?
Tabla: Clases clave de ficheros en Java
| Categoría | Clases principales |
|---|---|
| Rutas | File, Path, Paths |
| Operaciones | Files |
| Canales NIO | FileChannel, SeekableByteChannel |
| Buffers NIO | ByteBuffer, CharBuffer |
| Opciones de apertura | StandardOpenOption, StandardCopyOption |
| Bytes (legacy) | InputStream, OutputStream, FileInputStream, FileOutputStream |
| Caracteres (legacy) | Reader, Writer, FileReader, FileWriter, BufferedReader, BufferedWriter |
| Texto avanzado | PrintWriter, Scanner |
| Binario | DataInputStream, DataOutputStream |
| Objetos | Serializable, ObjectInputStream, ObjectOutputStream |
| Config/formatos | Properties, CSV (propio), JSON (Jackson/Gson), XML (JAXB) |
Mapa mental (MarkMap) del tema
Aplicación en el Mundo Real
En una aplicación web tipo e‑commerce, este tema se traduce en cosas como: logs de servidor (PrintWriter y Files), exportación de pedidos a CSV para contabilidad, almacenamiento en JSON de configuraciones de frontend y lectura de properties para configurar la conexión a la base de datos.
En un backend moderno, el flujo típico es: leer datos desde BBDD, convertirlos a DTO, serializarlos a JSON con Jackson y escribirlos en la respuesta HTTP; a la vez, guardar logs de acceso en ficheros rotativos y hacer backups comprimidos en ZIP para archivarlos.
Para Saber Más
- Documentación oficial de Java I/O (Path, Files, NIO): https://dev.java/learn/java-io/
- API oficial de
ObjectInputStream/ObjectOutputStream(serialización): https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/io/ObjectInputStream.html - Artículo sobre
File,PathyPathsen Java moderno: https://www.happycoders.eu/java/file-and-directory-names-file-path-paths/