1. Introducción
Recordemos un ejemplo de recorrido con Files.walk():
Path start = Paths.get("my-folder");
try (Stream<Path> stream = Files.walk(start)) {
stream.forEach(System.out::println);
}
Esto funciona muy bien si solo necesitas pasar por archivos y carpetas y hacer algo con ellos. Pero ¿qué pasa si la tarea es más compleja?
- Hay que eliminar de forma recursiva una carpeta junto con todos sus archivos y subdirectorios (y no solo si está vacía).
- Hay que copiar o mover un directorio de forma recursiva.
- Hay que recopilar estadísticas (por ejemplo, calcular el tamaño total de todos los archivos, agrupar archivos por extensión).
- Hay que manejar errores (por ejemplo, si no hay acceso a alguna carpeta, no queremos que todo falle).
En estos casos, el Stream API ya no es tan cómodo: hay que meter try-catch en las lambdas, vigilar el orden del recorrido (por ejemplo, borrar primero los archivos y luego las carpetas), y el código se vuelve poco legible.
Para estas tareas se ideó el mecanismo de recorrer el árbol del sistema de archivos usando FileVisitor.
2. Interfaz FileVisitor: cómo está estructurada
La interfaz FileVisitor<T> — es un «manejador de eventos» que recibe notificaciones sobre cada archivo y carpeta visitados durante el recorrido del sistema de archivos.
Cuando llamas a Files.walkFileTree(start, visitor), Java inicia el recorrido del árbol desde la ruta indicada y, en cada etapa, llama al método correspondiente de tu FileVisitor.
Métodos principales de la interfaz FileVisitor
Así es como luce la interfaz:
public interface FileVisitor<T> {
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException;
FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException;
FileVisitResult visitFileFailed(T file, IOException exc) throws IOException;
FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;
}
- preVisitDirectory — se llama antes de entrar en un directorio.
- visitFile — se llama para cada archivo.
- visitFileFailed — se llama si no se pudo acceder a un archivo.
- postVisitDirectory — se llama después de salir del directorio (es decir, tras procesar todos sus archivos y subdirectorios).
Cada uno de estos métodos devuelve un valor de tipo FileVisitResult, que determina cómo continuar el recorrido:
- FileVisitResult.CONTINUE — continuar el recorrido.
- FileVisitResult.SKIP_SUBTREE — omitir el directorio actual y todo lo que contiene.
- FileVisitResult.SKIP_SIBLINGS — omitir los «hermanos» restantes (archivos y carpetas en ese mismo nivel).
- FileVisitResult.TERMINATE — terminar el recorrido por completo.
Clase SimpleFileVisitor
Implementar los cuatro métodos cada vez es tedioso, sobre todo si solo necesitas uno o dos. Por eso en Java existe el práctico adaptador SimpleFileVisitor: ya implementa todos los métodos con un comportamiento «por defecto» (simplemente CONTINUE), y tú puedes sobrescribir solo los que necesites.
Ejemplo:
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class MyVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Archivo: " + file);
return FileVisitResult.CONTINUE;
}
}
3. Uso de Files.walkFileTree: ejemplo básico
Ejemplo 1: mostramos todos los archivos y carpetas
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class TreePrinter {
public static void main(String[] args) throws IOException {
Path start = Paths.get("my-folder");
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Carpeta: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(" Archivo: " + file);
return FileVisitResult.CONTINUE;
}
});
}
}
Salida:
Carpeta: my-folder
Archivo: my-folder/file1.txt
Archivo: my-folder/file2.txt
Carpeta: my-folder/subdir
Archivo: my-folder/subdir/nested.txt
Como se puede ver, el recorrido es «en profundidad»: primero entramos en la carpeta, luego procesamos sus archivos y después pasamos a las subcarpetas.
4. Ejemplo: eliminación recursiva de un directorio
Una de las tareas más frecuentes: eliminar una carpeta junto con todo su contenido. Si intentas eliminar una carpeta con Files.delete(path) y no está vacía, obtendrás una excepción. Primero hay que eliminar todos los archivos y subcarpetas, y después la propia carpeta.
Así se hace con FileVisitor:
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class RecursiveDelete {
public static void main(String[] args) throws IOException {
Path dirToDelete = Paths.get("test-folder");
Files.walkFileTree(dirToDelete, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file); // eliminamos el archivo
System.out.println("Archivo eliminado: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); // eliminamos la carpeta después de eliminar todo su contenido
System.out.println("Carpeta eliminada: " + dir);
return FileVisitResult.CONTINUE;
}
});
}
}
Punto importante:
La eliminación de carpetas ocurre en postVisitDirectory, es decir, después de eliminar todo su contenido. Si intentaras eliminar la carpeta antes de borrar los archivos que contiene, obtendrías un error.
5. Ejemplo: cálculo del tamaño total de todos los archivos en un directorio
Ahora escribamos un FileVisitor que calcule el tamaño total de todos los archivos en una carpeta y sus subcarpetas.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class DirectorySizeCalculator {
private static long totalSize = 0;
public static void main(String[] args) throws IOException {
Path start = Paths.get("my-folder");
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
totalSize += attrs.size();
return FileVisitResult.CONTINUE;
}
});
System.out.println("Tamaño total: " + totalSize + " bytes");
}
}
Ten en cuenta:
Usamos el campo totalSize para ir acumulando el tamaño. En aplicaciones reales es mejor evitar campos estáticos y pasar variables a través de objetos, pero para simplificar el ejemplo lo dejamos así.
6. Ejemplo: búsqueda de archivos por patrón (extensión) con FileVisitor
Supongamos que necesitamos encontrar todos los archivos .txt en el directorio y subdirectorios, y mostrar su lista.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class TxtFileFinder {
public static void main(String[] args) throws IOException {
Path start = Paths.get("my-folder");
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().endsWith(".txt")) {
System.out.println("Archivo .txt encontrado: " + file);
}
return FileVisitResult.CONTINUE;
}
});
}
}
Si quieres recopilar la lista de archivos encontrados, puedes crear una lista:
import java.util.ArrayList;
import java.util.List;
// dentro de main:
List<Path> txtFiles = new ArrayList<>();
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().endsWith(".txt")) {
txtFiles.add(file);
}
return FileVisitResult.CONTINUE;
}
});
System.out.println("Total encontrados: " + txtFiles.size());
7. Manejo de errores y particularidades del recorrido
¿Qué hacer si no hay acceso a un archivo o carpeta?
A veces durante el recorrido aparecen archivos o carpetas a los que no hay acceso (por ejemplo, si no tienes permisos o el archivo está en uso por otro proceso). En ese caso se llamará al método visitFileFailed.
Ejemplo:
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Error de acceso al archivo: " + file + " (" + exc + ")");
return FileVisitResult.CONTINUE; // continuamos el recorrido a pesar del error
}
Si quieres interrumpir el recorrido al producirse un error, devuelve TERMINATE.
¿Cómo omitir una carpeta por completo?
Si no quieres entrar en una carpeta concreta (por ejemplo, la carpeta .git o node_modules), puedes devolver SKIP_SUBTREE en el método preVisitDirectory:
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().equals("node_modules")) {
return FileVisitResult.SKIP_SUBTREE; // no entrar en esta carpeta ni en sus subdirectorios
}
return FileVisitResult.CONTINUE;
}
8. Práctica: implementamos nuestro propio FileVisitor
Implementemos un FileVisitor que:
- Encuentre todos los archivos con extensión .java en el directorio y subdirectorios,
- Cuente su cantidad,
- Calcule el tamaño total de esos archivos.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class JavaFilesStats {
public static void main(String[] args) throws IOException {
Path start = Paths.get("src"); // por ejemplo, el código fuente del proyecto
class JavaFileVisitor extends SimpleFileVisitor<Path> {
int count = 0;
long totalSize = 0;
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().endsWith(".java")) {
count++;
totalSize += attrs.size();
System.out.println("Archivo .java encontrado: " + file);
}
return FileVisitResult.CONTINUE;
}
}
JavaFileVisitor visitor = new JavaFileVisitor();
Files.walkFileTree(start, visitor);
System.out.println("Total de archivos .java: " + visitor.count);
System.out.println("Tamaño total: " + visitor.totalSize + " bytes");
}
}
9. Errores típicos al usar FileVisitor
Error n.º 1: intentar eliminar un directorio antes de eliminar los archivos que contiene. Si llamas a Files.delete(dir) en preVisitDirectory, obtendrás una excepción: primero hay que eliminar todos los archivos y subcarpetas, y solo después el directorio (hazlo en postVisitDirectory).
Error n.º 2: olvidar manejar los errores de acceso. Si no sobrescribes visitFileFailed, el programa puede terminar inesperadamente al encontrar un archivo protegido. Es mejor mostrar el error explícitamente y continuar el recorrido.
Error n.º 3: esperar que la operación «ocultar archivo» funcione igual en todos los SO. En Linux y macOS, un archivo se considera oculto si comienza con un punto (.gitignore), mientras que en Windows depende de un atributo especial. No confundas estos enfoques.
Error n.º 4: usar campos estáticos para acumular el resultado en aplicaciones multihilo. Si ejecutas varios recorridos en paralelo, los campos estáticos causarán confusión. Es mejor usar campos de instancia (o de una clase local, como en el ejemplo anterior).
Error n.º 5: olvidar cerrar recursos al trabajar con flujos dentro de FileVisitor. Si tu FileVisitor lee/escribe archivos, usa try-with-resources para evitar fugas.
GO TO FULL VERSION