Skip to content

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

  1. ¿Por qué es importante cerrar siempre un fichero después de usarlo?
  2. ¿Qué diferencias ves entre la ruta lógica ("logs/app.log") y la ruta absoluta?
  3. ¿Qué podría ocurrir si intentas crear un fichero en una carpeta que no existe?
  4. 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 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

  1. ¿Por qué un mismo entero ocupa más bytes en texto que en binario?
  2. ¿Qué tipo de fichero usarías para guardar logs de una aplicación y por qué?
  3. ¿Qué problemas de portabilidad pueden aparecer con ficheros binarios?
  4. 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

  1. ¿Cuándo usarías InputStream directamente y cuándo preferirías un Reader?
  2. ¿Qué ventaja aporta separar en bytes y caracteres?
  3. ¿Qué puede pasar si lees texto UTF‑8 suponiendo ISO‑8859‑1?
  4. 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

  1. ¿En qué tipo de utilidades de backend puede ser útil leer de System.in?
  2. ¿Qué diferencia funcional hay entre System.out y System.err?
  3. ¿Por qué es interesante no "mezclar" logs y salida de datos en la misma corriente?
  4. Diseña una mini‐herramienta que lea CSV de System.in y saque estadísticas por System.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

  1. ¿Qué ventaja tiene try-with-resources frente a cerrar en un bloque finally manual?
  2. Implementa un ejemplo donde olvides cerrar un fichero y observa el comportamiento.
  3. ¿Qué interfaz deben implementar los recursos para funcionar en try-with-resources?
  4. ¿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

  1. ¿Qué limitaciones tiene File comparado con Path?
  2. Escribe un método que liste solo los .txt de un directorio.
  3. ¿Qué podría devolver listFiles() si hay un error de permisos?
  4. ¿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

  1. ¿Qué ventajas tiene Path frente a File?
  2. ¿Qué hace el método normalize() sobre un Path?
  3. Escribe un método que reciba un Path y muestre sus segmentos uno a uno.
  4. ¿Cómo convertirías un Path a File y 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

  1. ¿Qué relación hay entre Path, Paths y FileSystems?
  2. ¿Qué estilo te parece más claro, Path.of o Paths.get? Justifica.
  3. Escribe un método que reciba un String y devuelva el Path usando ambas formas.
  4. ¿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

  1. ¿Qué diferencia hay entre Files.writeString y Files.write con lista de líneas?
  2. Implementa una función que copie un fichero utilizando Files.copy.
  3. ¿Cómo borrarías un directorio vacío usando Files?
  4. 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

  1. ¿Qué opciones adicionales ofrece StandardCopyOption?
  2. Implementa un método que haga un backup de un directorio completo.
  3. ¿Cómo listarías recursivamente todos los ficheros de un árbol de directorios?
  4. ¿En qué casos preferirías deleteIfExists frente a delete?



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

  1. ¿Por qué no puedes usar directamente Path.of para leer un recurso dentro del jar?
  2. Explica cómo organizarías en resources las plantillas HTML de una aplicación web.
  3. ¿Qué ventajas tiene empaquetar recursos dentro del jar?
  4. Implementa un método que devuelva un InputStream para 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

  1. ¿En qué situaciones reales usarías ficheros temporales en una aplicación web?
  2. ¿Cómo te asegurarías de borrar los temporales al final de su uso?
  3. ¿Qué diferencias hay entre createTempFile y createTempDirectory?
  4. 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

  1. ¿Por qué puede ser útil comprimir logs antes de guardarlos a largo plazo?
  2. ¿Qué cuidados de seguridad tendrías al descomprimir ZIPs subidos por usuarios?
  3. Implementa un método que comprima todos los .txt de un directorio en un único ZIP.
  4. ¿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

  1. ¿Qué problemas concretos del modelo java.io resuelve java.nio?
  2. ¿En qué situaciones seguirías necesitando java.io en un proyecto moderno?
  3. Investiga qué es el "zero-copy" y por qué Files.copy puede ser más rápido que copiar con buffer manual.
  4. ¿Qué relación hay entre Path, Files y FileChannel en 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:

  1. Escribir datos en el buffer: buffer.put(data) o channel.read(buffer)
  2. Flip (voltear): buffer.flip() — prepara el buffer para lectura, moviendo el límite a la posición actual y la posición a 0
  3. Leer datos del buffer: buffer.get() o channel.write(buffer)
  4. Clear/Compact: buffer.clear() para reutilizarlo, o buffer.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

  1. ¿Qué ventaja tiene el modelo Channel+Buffer frente al modelo Stream de java.io?
  2. ¿Qué ocurre si olvidas llamar a flip() antes de leer del buffer?
  3. ¿Cuál es la diferencia entre ByteBuffer.allocate() y ByteBuffer.allocateDirect()?
  4. Implementa una copia de fichero binario usando FileChannel y ByteBuffer con 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

  1. ¿Qué significa "zero-copy" y por qué mejora el rendimiento?
  2. ¿Cuándo usarías transferTo en lugar de Files.copy?
  3. ¿Qué ventajas tiene transferTo frente a copiar manualmente con un ByteBuffer?
  4. 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

  1. ¿Cuándo usarías readString frente a lines?
  2. ¿Qué pasa si usas writeString sin APPEND sobre un fichero que ya tiene datos?
  3. ¿Por qué Files.lines devuelve un Stream en vez de una List?
  4. 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

  1. ¿Qué diferencia hay entre Files.newBufferedReader y new BufferedReader(new FileReader(...))?
  2. ¿Cuándo necesitarías un BufferedWriter en lugar de Files.writeString?
  3. ¿Qué pasa si no especificas codificación en newBufferedReader?
  4. Implementa un método que copie un fichero de texto línea a línea usando newBufferedReader y newBufferedWriter.



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

  1. ¿Qué diferencia hay entre print, println y printf en PrintWriter?
  2. ¿Cuándo preferirías Scanner frente a Files.lines() para leer un fichero?
  3. Implementa una lectura de fichero CSV con Scanner usando ; como delimitador.
  4. ¿Qué excepciones lanza Scanner si 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

  1. ¿Qué ventajas tiene usar Files.newOutputStream(path) frente a new FileOutputStream(path.toFile())?
  2. Explica qué podría pasar si cambias el orden de lectura respecto al de escritura en DataInputStream.
  3. Implementa una función que lea múltiples registros secuenciales desde un fichero binario de primitivos.
  4. ¿Por qué no existe un DataChannel en NIO? ¿Cómo se resuelve esto con ByteBuffer?




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

  1. ¿Por qué es peligroso usar readAllLines con un fichero de 10 GB?
  2. ¿Qué diferencia hay entre un Stream<String> lazy y una List<String> cargada en memoria?
  3. Implementa un buscador de texto que reciba un fichero y una palabra y devuelva las líneas que la contengan.
  4. ¿Qué codificación usa Files.readString si 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

  1. ¿Por qué Files.lines es más eficiente que readAllLines para ficheros grandes?
  2. ¿Qué problemas tiene usar split(",") en un CSV real con campos entrecomillados?
  3. Implementa una función que devuelva solo las primeras N líneas de un fichero usando Files.lines().limit(n).
  4. ¿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

  1. ¿Qué pasa si usas Files.writeString sin APPEND sobre un fichero que ya tiene datos?
  2. ¿Cómo escribirías texto usando una codificación diferente a UTF-8?
  3. Implementa un método que reciba una List<String> y la escriba en un fichero, una palabra por línea.
  4. ¿Cuándo elegirías newBufferedWriter frente a Files.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

  1. ¿Por qué es buena idea usar un fichero temporal para modificaciones?
  2. ¿Qué estrategias de backup aplicarías a ficheros críticos de configuración?
  3. ¿Cómo gestionarías logs rotativos (log diario) con Java?
  4. 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

  1. ¿Qué ventaja clara ofrece un fichero binario sobre uno de texto para guardar muchos enteros?
  2. ¿Qué pasa si intentas abrir un binario con un editor de texto y lo modificas?
  3. ¿Por qué es tan importante documentar la estructura de un fichero binario?
  4. 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 un byte[]. 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

  1. ¿Cuándo usarías readAllBytes y cuándo FileChannel?
  2. ¿Qué sucede si intentas readAllBytes con un fichero de 4 GB?
  3. ¿Cómo detectarías si la estructura del fichero binario es la esperada (por ejemplo, leyendo un "magic number")?
  4. 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

  1. ¿Qué diferencia hay entre Files.write(path, bytes) y usar FileChannel?
  2. ¿Por qué usamos Integer.BYTES para calcular el tamaño del buffer?
  3. Diseña un formato binario para almacenar registros compactos de cliente (nombre fijo 20 chars + edad int + saldo double).
  4. ¿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 Java
  • allocateDirect(capacity): crea un buffer fuera del heap, más rápido para E/S pero más costoso de crear
  • wrap(byte[]): envuelve un array existente
  • flip(): prepara para lectura después de escribir
  • clear(): reinicia posición y límite para reutilizar
  • compact(): mueve los datos no leídos al principio

Preguntas / ejercicios

Question

  1. ¿Qué diferencia hay entre ByteBuffer.allocate y ByteBuffer.allocateDirect?
  2. ¿Cuándo es mejor usar ByteBuffer frente a DataOutputStream para datos binarios?
  3. ¿Qué pasa si sumas 1 a 0xFF sin hacer & 0xFF?
  4. 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

  1. ¿Qué ventaja tiene el acceso aleatorio frente al secuencial en grandes ficheros de registros?
  2. ¿Por qué suele requerir registros de tamaño fijo?
  3. Pon un ejemplo de aplicación real que se beneficie de acceso aleatorio.
  4. ¿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

  1. ¿Qué diferencia hay entre channel.position(n) y el seek(n) de RandomAccessFile?
  2. ¿Cómo calcularías la posición de un registro dado su índice y tamaño?
  3. Diseña la estructura de un registro fijo para un sistema de reservas de hotel.
  4. ¿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

  1. ¿Qué ventajas tiene serializar objetos directamente frente a usar DTOs + JSON?
  2. ¿Qué riesgos de seguridad conoces asociados a la deserialización?
  3. ¿Por qué no es buena idea deserializar datos que vienen de clientes sin control?
  4. 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

  1. ¿Por qué Serializable no define métodos?
  2. ¿Qué utilidad tiene la palabra clave transient?
  3. ¿Qué pasa si cambias la estructura de la clase sin actualizar serialVersionUID?
  4. ¿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

  1. ¿Qué ocurre si intentas serializar una clase que no implementa Serializable?
  2. ¿Qué excepción puede lanzar readObject además de IOException?
  3. ¿Cómo se comporta la serialización con relaciones entre objetos (por ejemplo, listas)?
  4. 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

  1. ¿Qué ventajas tiene serializar una lista completa frente a serializar elementos sueltos?
  2. ¿Cómo leerías todos los objetos de un fichero hasta final de stream?
  3. ¿Por qué es problemático escribir múltiples cabeceras de ObjectOutputStream?
  4. ¿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

  1. ¿Cómo detectas el final del fichero cuando lees objetos secuenciales?
  2. ¿Qué problemas puede causar cambiar la versión de la clase entre serialización y deserialización?
  3. Diseña una función que devuelva un List<Alumno> desde un fichero de objetos.
  4. ¿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

  1. ¿Cómo ayuda serialVersionUID a manejar cambios de versión de una clase?
  2. ¿Qué políticas de seguridad aplicarías en un servidor que recibe objetos serializados?
  3. ¿Por qué muchas aplicaciones modernas prefieren JSON o XML a la serialización nativa?
  4. 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

  1. ¿Por qué es buena idea separar DTOs de entidades de dominio?
  2. ¿Qué problemas se evitan usando DTOs en lugar de serializar entidades directamente?
  3. Diseña un DTO para un pedido en una tienda online.
  4. ¿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

  1. ¿Por qué es útil separar configuración de código usando properties?
  2. ¿Cómo gestionarías diferentes ficheros de properties para desarrollo y producción?
  3. Diseña un app.properties mínimo para una aplicación web.
  4. ¿Qué diferencia hay entre getProperty y get en Properties?



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

  1. ¿Qué problemas tiene el ejemplo "naive" de parseo de CSV con split(",")?
  2. ¿Qué mejoras harías al escribir CSV (por ejemplo, escapado de comillas)?
  3. Diseña un CSV de pedidos de tienda online con cabecera y dos registros.
  4. ¿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

  1. ¿Qué ventajas tiene JSON frente a XML y CSV?
  2. ¿Por qué es cómodo usar librerías como Jackson en lugar de manejar JSON a mano?
  3. Diseña la estructura JSON para un usuario con dirección anidada.
  4. ¿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

  1. ¿Qué ventajas ofrece XML sobre JSON en ciertos contextos (por ejemplo, validación con XSD)?
  2. ¿Qué te parece más legible, JSON o XML? Justifica.
  3. Diseña un pequeño XML para representar una lista de productos.
  4. ¿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, Path y Paths en Java moderno: https://www.happycoders.eu/java/file-and-directory-names-file-path-paths/ [^10][^11][^7][^8][^9]