1. NIO2: conociéndolo con más detalle
Ya nos hemos familiarizado con NIO2, pero ahora vamos a repasar y profundizar en esta útil biblioteca para trabajar con archivos y directorios.
Antes en Java solo existía la clase File. Sabía comprobar la existencia de un archivo, crear y eliminar archivos y carpetas, obtener la lista de archivos de un directorio. Pero tenía muchas limitaciones:
- trabajar con rutas era incómodo, especialmente si había que tener en cuenta distintos sistemas operativos (C:\Users\user\file.txt en Windows y /home/user/file.txt en Linux);
- no había un soporte adecuado para enlaces simbólicos, permisos y atributos de archivos;
- las posibilidades de recorrer el árbol de directorios eran limitadas;
- el manejo de errores dejaba mucho que desear.
Con la llegada de NIO2 (New Input/Output, versión 2) en Java 7, la vida del desarrollador se volvió más fácil y agradable. Ahora tenemos a nuestra disposición:
- la clase Path para trabajar cómodamente con rutas de archivos y carpetas;
- la clase Files, que proporciona todas las operaciones principales: lectura, escritura, copia, eliminación y obtención de información sobre archivos;
- la interfaz FileVisitor y métodos como Files.walk, que permiten recorrer el sistema de archivos de forma sencilla y flexible.
¿Por qué es importante?
- Multiplataforma: El mismo código funciona en Windows, Linux y macOS, sin pensar en los separadores (/ o \).
- Seguridad y comodidad: Más información sobre los errores, menos magia y menos sorpresas.
- Potencia: Puedes procesar directorios enormes e incluso recorrer de forma recursiva con filtrado y procesamiento en paralelo.
2. Clases principales: Path y Files
Clase Path
Path es la representación moderna de una ruta a un archivo o carpeta. No tiene por qué señalar a un archivo realmente existente: es simplemente una ruta con la que resulta cómodo trabajar.
Obtener un Path
import java.nio.file.Path;
import java.nio.file.Paths;
Path path1 = Paths.get("file.txt"); // ruta relativa
Path path2 = Paths.get("/home/user/file.txt"); // ruta absoluta
Path path3 = Path.of("mydir", "subdir", "file.txt"); // desde Java 11+
Hecho: Path es independiente del sistema operativo. ¡Olvídate de concatenar cadenas manualmente con / o \!
Conversión a cadena
System.out.println(path1.toString());
Obtener el directorio padre y el nombre de archivo
Path parent = path1.getParent(); // puede ser null para rutas relativas
Path fileName = path1.getFileName(); // solo el nombre del archivo
Clase Files
Files es un conjunto de métodos estáticos para todas las operaciones con archivos y directorios:
- Comprobación de existencia: Files.exists(path)
- Lectura y escritura de archivos: Files.readAllBytes(path), Files.write(path, bytes)
- Obtención de información: Files.size(path), Files.getLastModifiedTime(path)
- Copia, eliminación, movimiento: Files.copy, Files.delete, Files.move
Ejemplos:
import java.nio.file.Files;
import java.nio.file.Path;
Path path = Path.of("file.txt");
if (Files.exists(path)) {
System.out.println("¡El archivo existe!");
System.out.println("Tamaño: " + Files.size(path) + " bytes");
System.out.println("Última modificación: " + Files.getLastModifiedTime(path));
} else {
System.out.println("Archivo no encontrado.");
}
3. Recorrido del sistema de archivos: Files.walk y compañía
El problema del método antiguo
En el API antiguo, para recorrer todos los archivos de una carpeta y sus subcarpetas, había que escribir funciones recursivas, comprobar manualmente qué era archivo y qué era carpeta, y asegurarse de no caer en una recursión infinita. No solo era tedioso, sino que además era fácil cometer errores.
Método moderno: Files.walk
Files.walk(Path start) devuelve un Stream<Path>: un flujo de todos los archivos y carpetas a partir de la ruta indicada, incluidos todos los subdirectorios. ¡Ahora recorrer el sistema de archivos es simplemente trabajar con streams!
Ejemplo: listar todos los archivos y carpetas
import java.nio.file.*;
try (var paths = Files.walk(Path.of("mydir"))) {
paths.forEach(System.out::println);
}
Aquí se imprimirán todas las rutas: tanto archivos como carpetas, empezando por mydir.
Ejemplo: solo archivos (sin carpetas)
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
El método Files.isRegularFile(path) devolverá true solo para archivos regulares (no carpetas, no symlinks).
Ejemplo: búsqueda de archivos por extensión
Supongamos que necesitamos encontrar todos los archivos .txt en el directorio y subdirectorios:
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
Ejemplo: cálculo del tamaño total de todos los archivos
long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
totalSize = paths.filter(Files::isRegularFile)
.mapToLong(path -> {
try {
return Files.size(path);
} catch (Exception e) {
System.err.println("Error al leer el tamaño: " + path);
return 0L;
}
})
.sum();
}
System.out.println("Tamaño total de los archivos: " + totalSize + " bytes");
¡Importante!
- El método Files.walk devuelve un flujo que hay que cerrar (implementa AutoCloseable). Por eso usamos try-with-resources.
- Por defecto, la profundidad del recorrido llega hasta el fondo (todos los subdirectorios). Se puede limitar la profundidad: Files.walk(path, maxDepth)
4. Tareas prácticas
Tarea 1: encontrar todas las imágenes en el directorio
Hay que encontrar todos los archivos con extensión .jpg, .png, .gif en la carpeta images y mostrar sus nombres.
import java.nio.file.*;
import java.util.Set;
Set<String> extensions = Set.of(".jpg", ".png", ".gif");
try (var paths = Files.walk(Path.of("images"))) {
paths.filter(Files::isRegularFile)
.filter(path -> {
String name = path.getFileName().toString().toLowerCase();
return extensions.stream().anyMatch(name::endsWith);
})
.forEach(System.out::println);
}
Tarea 2: copiar todos los archivos .txt a otra carpeta
import java.nio.file.*;
Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");
try (var paths = Files.walk(sourceDir)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
try {
Path relative = sourceDir.relativize(path);
Path target = destDir.resolve(relative);
Files.createDirectories(target.getParent());
Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copiado: " + path + " -> " + target);
} catch (Exception e) {
System.err.println("Error de copia: " + path);
}
});
}
Aquí conservamos la estructura de subdirectorios.
5. Detalles útiles
Ventajas de NIO2
Multiplataforma
Path se encarga él solo de los separadores de carpetas. Tu código funcionará igual en Windows, Linux y macOS.
Procesamiento en flujo
Métodos como Files.walk devuelven un flujo (Stream<Path>) que puedes filtrar, transformar y recopilar en colecciones: todo lo que permite el Stream API.
Trabajo con directorios grandes
El API antiguo podía caerse si había demasiados archivos (por ejemplo, 100 000 fotos). NIO2 maneja estos casos con facilidad, ya que no carga todo en memoria de una vez.
Soporte de symlinks, atributos y permisos
Puedes saber si una ruta es un enlace simbólico (Files.isSymbolicLink(path)), obtener permisos (Files.getPosixFilePermissions(path)), conocer el propietario del archivo y mucho más.
Comparación entre el API antiguo y el nuevo
| Operación | API antiguo (File) | API nuevo (Path, Files) |
|---|---|---|
| Comprobar existencia | |
|
| Obtener tamaño | |
|
| Lista de archivos en una carpeta | |
|
| Recorrido recursivo | Recursión manual | |
| Copiar archivo | file.renameTo() (poco fiable) | |
| Obtener extensión | Analizar la cadena | |
| Obtener el padre | |
|
| Trabajo con permisos | Casi nada | |
Aspectos importantes
Comprobación del tipo de archivo
- Files.isRegularFile(path): archivo normal
- Files.isDirectory(path): carpeta
- Files.isSymbolicLink(path): symlink
Trabajo con directorios grandes
- No conviene recopilar todas las rutas en una lista: trabaja con flujos (Stream<Path>) y procesa a medida que llegan.
- Al terminar, el flujo debe cerrarse (try-with-resources).
Excepciones
- Casi todos los métodos pueden lanzar IOException: no olvides gestionar los errores (o propagarlos).
Limitación de la profundidad del recorrido
try (var paths = Files.walk(Path.of("mydir"), 2)) { // solo 2 niveles
// ...
}
6. Errores típicos al trabajar con NIO2
Error n.º 1: olvidar cerrar el flujo de walk. Si no usas try-with-resources, puedes provocar una fuga de recursos: el flujo del sistema de archivos quedará abierto. Utiliza siempre la construcción try (var paths = Files.walk(...)) { ... }.
Error n.º 2: no comprobar que la ruta es un directorio. Si pasas a Files.walk una ruta a un archivo en lugar de a una carpeta, puedes obtener un comportamiento inesperado o un error.
Error n.º 3: no gestionar las excepciones. Prácticamente todos los métodos de NIO2 pueden lanzar IOException. No dejes estos errores sin atención: al menos muestra un mensaje al usuario o regístralo en logs.
Error n.º 4: confusión con los separadores de rutas. Si concatenas rutas manualmente con / o \, ¡no lo hagas! Usa Path.of(...) o resolve(...): se encargarán de hacerlo bien en cualquier SO.
Error n.º 5: intentar cargar un directorio enorme «en memoria». No recopiles todas las rutas en una lista si hay muchísimos archivos: trabaja con flujos (Stream<Path>) y procésalos a medida que llegan.
Error n.º 6: olvidarse de la multiplataforma. No dejes rutas absolutas hardcodeadas con estilo Windows o Unix. Usa Path y operaciones como resolve/relativize: harán lo correcto en cualquier SO.
GO TO FULL VERSION