java-io-nio-files-path-lectura-escritura

Ficheros en Java con Path y Files (NIO.2)

  • 4 min

La API java.nio.file proporciona tipos y operaciones modernas para trabajar con rutas y sistemas de archivos.

Durante muchos años, leer un fichero en Java implicaba combinar varias clases, como en new BufferedReader(new FileReader(new File(...))).

Desde Java 7 tenemos NIO.2, cuya API de rutas vive en java.nio.file. Los métodos habituales de Files son bloqueantes; las operaciones asíncronas se ofrecen mediante tipos específicos como AsynchronousFileChannel.

Vamos a aprender a manejar el sistema de archivos con Path y Files.

Los cimientos: Path y Files

En la API moderna, hay dos protagonistas absolutos:

La interfaz Path

Es la alternativa moderna a java.io.File para representar rutas, aunque ambas APIs siguen existiendo y pueden convertirse entre sí. Un Path no es el fichero en sí, es su dirección en el disco duro. Es una ruta, que puede existir o no.

import java.nio.file.Path;
import java.nio.file.Paths;

// Forma clásica (Java 7+)
Path ruta = Paths.get("datos", "usuarios.txt"); // datos/usuarios.txt

// Forma moderna (Java 11+)
Path rutaModerna = Path.of("C:", "Proyectos", "notas.txt");

// Utilidades de rutas
System.out.println(rutaModerna.getFileName()); // notas.txt
System.out.println(rutaModerna.getParent());   // C:\Proyectos
System.out.println(rutaModerna.toAbsolutePath()); // Resuelve la ruta completa
Copied!

Path.of() compone una ruta con el separador del sistema. Aun así, una ruta absoluta de Windows no se convierte automáticamente en una ruta válida de Linux o macOS.

La clase Files

Es una clase de utilidad estática (como Collections o Math) que tiene métodos para hacer todo lo que necesites con un Path.

import java.nio.file.Files;

Path p = Path.of("documento.txt");

// Chequeos básicos
boolean existe = Files.exists(p);
boolean esDirectorio = Files.isDirectory(p);

// Operaciones de gestión (Requieren try-catch por IOException)
try {
    if (!exists) {
        Files.createFile(p); // Crear vacío
    }
    Files.copy(p, Path.of("copia.txt")); // Copiar
    Files.move(p, Path.of("movido.txt")); // Mover/Renombrar
    Files.delete(p); // Borrar
} catch (IOException e) {
    e.printStackTrace();
}
Copied!

Lectura de ficheros

Aquí es donde la API moderna brilla. Dependiendo del tamaño del fichero, usamos una estrategia u otra.

Caso a: ficheros pequeños (todo a memoria)

Si el fichero pesa unos pocos KB o MB, podemos leerlo entero de golpe.

Path ruta = Path.of("config.json");

try {
    // Leer todo el contenido como un solo String (Java 11+)
    String contenido = Files.readString(ruta);

    // Leer todas las líneas como una Lista de Strings
    List<String> lineas = Files.readAllLines(ruta);

} catch (IOException e) { ... }
Copied!

¡Cuidado con la RAM! No uses readAllLines con un fichero que pueda superar la memoria disponible. El método carga todas las líneas y podría provocar un OutOfMemoryError.

Caso b: ficheros grandes (streams y buffers)

Para ficheros grandes, debemos leer línea a línea (flujo) sin cargarlo todo. Aquí combinamos NIO con los Streams que aprendimos en el bloque anterior.

Path rutaLog = Path.of("server.log"); // Imagina que pesa 10GB

// Usamos try-with-resources para asegurar que el fichero se cierra
try (Stream<String> lineas = Files.lines(rutaLog)) {

    lineas.filter(l -> l.contains("ERROR")) // Procesamos al vuelo
          .forEach(System.out::println);

} catch (IOException e) { ... }
Copied!

¡Fíjate qué potencia! Estamos procesando un log gigante línea por línea con un consumo de memoria mínimo.

Escritura de ficheros

Escribir es igual de sencillo con la clase Files.

Path destino = Path.of("salida.txt");
String texto = "Hola mundo desde Java";

try {
    // 1. Escritura simple (Sobrescribe si existe)
    Files.writeString(destino, texto);

    // 2. Escritura de una lista de líneas
    List<String> lista = List.of("Línea 1", "Línea 2");
    Files.write(destino, lista);

    // 3. Añadir al final (APPEND) en vez de sobrescribir
    Files.writeString(destino, "\nNueva línea", StandardOpenOption.APPEND);

} catch (IOException e) { ... }
Copied!

Serialización: el pasado vs el presente

En los tutoriales antiguos de Java verás mucho sobre la interfaz Serializable.

La Serialización Nativa de Java consiste en convertir un objeto (instancia en memoria) en una secuencia de bytes (.ser) para guardarlo en disco y recuperarlo después.

// CLÁSICO (Desaconsejado hoy en día)
// La clase debe implementar Serializable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("dato.ser"))) {
    out.writeObject(miObjeto);
}
Copied!

¿Por qué ya no se usa tanto?

  1. Es Java-dependiente: Un fichero .ser solo lo entiende Java. No puedes leerlo con Python o JavaScript.
  2. Es frágil: Si cambias la clase (añades un atributo), la deserialización falla a menos que gestiones versiones (serialVersionUID).
  3. Seguridad: Ha sido fuente de muchas vulnerabilidades críticas en Java.

La alternativa moderna: JSON

Hoy en día, para guardar objetos o enviarlos por red, usamos JSON. Es texto plano, legible por humanos y compatible con cualquier lenguaje.

En Java, usamos librerías externas estándar como Jackson o Gson.

// Ejemplo conceptual con Jackson
ObjectMapper mapper = new ObjectMapper();

// Guardar objeto como JSON en un fichero
mapper.writeValue(new File("producto.json"), miProducto);

// Leer JSON a objeto
Producto p = mapper.readValue(new File("producto.json"), Producto.class);
Copied!

Aunque JSON no viene nativo en el JDK (tienes que añadir la librería), es el estándar de facto en la industria.