1. Introduction
Rappelons un exemple de parcours à l’aide de Files.walk() :
Path start = Paths.get("my-folder");
try (Stream<Path> stream = Files.walk(start)) {
stream.forEach(System.out::println);
}
Cela fonctionne très bien si vous avez simplement besoin d’itérer sur des fichiers et des dossiers et de faire quelque chose avec eux. Mais que faire si la tâche est plus complexe ?
- Il faut supprimer récursivement un dossier avec tous ses fichiers et sous-répertoires (et pas seulement s’il est vide).
- Il faut copier ou déplacer un répertoire de manière récursive.
- Il faut collecter des statistiques (par exemple, calculer la taille totale de tous les fichiers, regrouper les fichiers par extensions).
- Il faut gérer les erreurs (par exemple, si un dossier n’est pas accessible, on ne veut pas que tout s’arrête brutalement).
Dans ces cas, le Stream API est moins pratique : il faut ajouter des try-catch dans les lambdas, surveiller l’ordre de parcours (par exemple, supprimer d’abord les fichiers puis les dossiers), et le code devient illisible.
Pour ces tâches, on utilise le mécanisme de parcours d’arborescence du système de fichiers via FileVisitor.
2. Interface FileVisitor : fonctionnement
L’interface FileVisitor<T> est un « gestionnaire d’événements » qui reçoit des notifications pour chaque fichier et dossier visité lors du parcours du système de fichiers.
Lorsque vous appelez Files.walkFileTree(start, visitor), Java commence le parcours de l’arborescence à partir du chemin indiqué et, à chaque étape, invoque la méthode correspondante de votre FileVisitor.
Méthodes principales de l’interface FileVisitor
Voici à quoi ressemble l’interface :
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 — appelée avant d’entrer dans un répertoire.
- visitFile — appelée pour chaque fichier.
- visitFileFailed — appelée si l’accès à un fichier échoue.
- postVisitDirectory — appelée après être sorti d’un répertoire (c’est-à-dire après traitement de tous ses fichiers et sous-répertoires).
Chacune de ces méthodes renvoie une valeur de type FileVisitResult, qui définit la manière de poursuivre le parcours :
- FileVisitResult.CONTINUE — continuer le parcours.
- FileVisitResult.SKIP_SUBTREE — ignorer le répertoire courant et tout son contenu.
- FileVisitResult.SKIP_SIBLINGS — ignorer les « frères » restants (fichiers et dossiers à ce niveau).
- FileVisitResult.TERMINATE — interrompre complètement le parcours.
Classe SimpleFileVisitor
Implémenter les quatre méthodes à chaque fois est fastidieux, surtout si vous n’avez besoin que d’une ou deux. Java fournit donc une classe adaptatrice pratique SimpleFileVisitor : elle implémente déjà toutes les méthodes avec un comportement par défaut (« CONTINUE »), et vous pouvez ne surcharger que celles qui vous intéressent.
Exemple :
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("Fichier : " + file);
return FileVisitResult.CONTINUE;
}
}
3. Utilisation de Files.walkFileTree : exemple de base
Exemple 1 : afficher tous les fichiers et dossiers
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("Dossier : " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(" Fichier : " + file);
return FileVisitResult.CONTINUE;
}
});
}
}
Sortie :
Dossier: my-folder
Fichier: my-folder/file1.txt
Fichier: my-folder/file2.txt
Dossier: my-folder/subdir
Fichier: my-folder/subdir/nested.txt
Comme vous le voyez, le parcours est en profondeur : on entre d’abord dans un dossier, on traite ensuite ses fichiers, puis on passe aux sous-dossiers.
4. Exemple : suppression récursive d’un répertoire
L’une des tâches les plus fréquentes : supprimer un dossier avec tout son contenu. Si vous essayez de supprimer un dossier avec Files.delete(path) alors qu’il n’est pas vide, vous obtiendrez une exception. Il faut d’abord supprimer tous les fichiers et sous-dossiers, puis le dossier lui‑même.
Voici comment le faire avec 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); // on supprime le fichier
System.out.println("Fichier supprimé : " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); // on supprime le dossier après avoir tout supprimé à l’intérieur
System.out.println("Dossier supprimé : " + dir);
return FileVisitResult.CONTINUE;
}
});
}
}
Point important :
La suppression des dossiers se fait dans postVisitDirectory, c’est‑à‑dire après avoir supprimé tout le contenu. Si nous avions tenté de supprimer le dossier avant de supprimer les fichiers qu’il contient, nous aurions eu une erreur.
5. Exemple : calcul de la taille totale de tous les fichiers d’un répertoire
Écrivons maintenant un FileVisitor qui calcule la taille totale de tous les fichiers d’un dossier et de ses sous-dossiers.
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("Taille totale : " + totalSize + " octets");
}
}
Remarque :
Nous utilisons le champ totalSize pour accumuler la taille. Dans les applications réelles, il est préférable d’éviter les champs statiques et de passer les variables via des objets, mais pour la simplicité de l’exemple, cela convient.
6. Exemple : recherche de fichiers par extension avec FileVisitor
Supposons que nous devions trouver tous les fichiers .txt dans un répertoire et ses sous-répertoires, et afficher leur liste.
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("Fichier .txt trouvé : " + file);
}
return FileVisitResult.CONTINUE;
}
});
}
}
Si vous souhaitez collecter la liste des fichiers trouvés, vous pouvez créer une liste :
import java.util.ArrayList;
import java.util.List;
// dans 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 trouvés : " + txtFiles.size());
7. Gestion des erreurs et particularités du parcours
Que faire si l’accès à un fichier ou à un dossier est impossible ?
Il arrive parfois que l’on rencontre des fichiers ou des dossiers inaccessibles (par exemple si vous n’avez pas les droits, ou si un autre processus verrouille le fichier). Dans ce cas, la méthode visitFileFailed sera appelée.
Exemple :
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Erreur d’accès au fichier : " + file + " (" + exc + ")");
return FileVisitResult.CONTINUE; // on continue le parcours malgré l’erreur
}
Si vous souhaitez interrompre le parcours en cas d’erreur, retournez TERMINATE.
Comment ignorer un dossier entier ?
Si vous ne voulez pas entrer dans un dossier spécifique (par exemple, le dossier .git ou node_modules), vous pouvez renvoyer SKIP_SUBTREE dans la méthode preVisitDirectory :
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().equals("node_modules")) {
return FileVisitResult.SKIP_SUBTREE; // ne pas entrer dans ce dossier ni ses sous-répertoires
}
return FileVisitResult.CONTINUE;
}
8. Pratique : implémentons notre propre FileVisitor
Implémentons un FileVisitor qui :
- Trouve tous les fichiers avec l’extension .java dans un répertoire et ses sous-répertoires,
- Compte leur nombre,
- Calcule la taille totale de ces fichiers.
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"); // par exemple, le code source du projet
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("Fichier .java trouvé : " + file);
}
return FileVisitResult.CONTINUE;
}
}
JavaFileVisitor visitor = new JavaFileVisitor();
Files.walkFileTree(start, visitor);
System.out.println("Nombre total de fichiers .java : " + visitor.count);
System.out.println("Taille totale : " + visitor.totalSize + " octets");
}
}
9. Erreurs typiques lors de l’utilisation de FileVisitor
Erreur n° 1 : tentative de supprimer un répertoire avant de supprimer les fichiers qu’il contient. Si vous appelez Files.delete(dir) dans preVisitDirectory, vous obtiendrez une exception — il faut d’abord supprimer tous les fichiers et sous-dossiers, puis le répertoire lui‑même (faites‑le dans postVisitDirectory).
Erreur n° 2 : oublier de gérer les erreurs d’accès. Si vous ne surchargez pas visitFileFailed, le programme peut se terminer de manière inattendue en rencontrant un fichier protégé. Il vaut mieux afficher explicitement l’erreur et poursuivre le parcours.
Erreur n° 3 : s’attendre à ce que l’opération « masquer un fichier » fonctionne de la même manière sur tous les OS. Sous Linux et macOS, un fichier est considéré comme caché s’il commence par un point (.gitignore), tandis que sous Windows c’est un attribut spécifique. Ne confondez pas ces approches.
Erreur n° 4 : utiliser des champs statiques pour accumuler des résultats dans des applications multithread. Si vous lancez plusieurs parcours en parallèle, les champs statiques provoqueront de la confusion. Préférez des champs d’instance (ou de classe locale, comme dans l’exemple ci‑dessus).
Erreur n° 5 : oublier de fermer les ressources lors de l’utilisation de flux au sein d’un FileVisitor. Si votre FileVisitor lit/écrit des fichiers, utilisez try-with-resources afin d’éviter les fuites.
GO TO FULL VERSION