W3docs

Parcourir les arborescences de fichiers en Java

Parcourez récursivement les répertoires en Java avec Files.walk, Files.find et l'interface FileVisitor.

Le chapitre précédent s'est terminé avec Files.walk(dir) — la forme Stream<Path> de « donne-moi tous les fichiers sous ce répertoire ». C'est l'outil rapide pour le cas courant. Ce chapitre couvre l'alternative de plus bas niveau, Files.walkFileTree, qui vous permet de contrôler la traversée d'une manière que la forme flux ne peut pas : gérer les erreurs d'E/S fichier par fichier, ignorer des sous-arborescences entières en plein parcours, exécuter du code à la sortie d'un répertoire aussi bien qu'à l'entrée, et court-circuiter dès une correspondance trouvée.

Utilisez Files.walk pour « lister tout ». Utilisez Files.walkFileTree pour « effectuer une action à chaque étape, avec contrôle sur l'étape ».

Trois API de parcours

Le catalogue, dans l'ordre de fréquence d'utilisation :

APIRetourneQuand
Files.walk(dir)Stream<Path>Le plus courant — filter/map/foreach sur chaque entrée
Files.find(dir, depth, biPredicate)Stream<Path>Idem, avec un prédicat tenant compte des attributs (isDirectory, mtime)
Files.walkFileTree(dir, visitor)Path (le point de départ)Besoin de hooks pré/post-visite, gestion d'erreur par fichier, ou pour interrompre le parcours

Les deux premières suffisent pour 90 % du code « trouve-moi tous les fichiers .log ». walkFileTree est ce qu'il faut utiliser quand la réponse est « et ensuite supprimer le répertoire » ou « arrêter le parcours dès que le premier fichier correspondant est trouvé ».

FileVisitor et SimpleFileVisitor

Files.walkFileTree prend un FileVisitor<Path> — une interface avec quatre méthodes que le parcoureur appelle à des moments précis :

FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs);    // entering a directory
FileVisitResult visitFile(Path file, BasicFileAttributes attrs);            // each non-directory entry
FileVisitResult visitFileFailed(Path file, IOException exc);                // I/O failure on a specific file
FileVisitResult postVisitDirectory(Path dir, IOException exc);              // leaving the directory (after all children)

L'ordre est important : pour un répertoire d avec les enfants [a, b/, c], les appels sont preVisitDirectory(d), visitFile(a), preVisitDirectory(b), ... postVisitDirectory(b), visitFile(c), postVisitDirectory(d). Le hook post* est ce qui rend la suppression récursive possible — vous ne pouvez pas supprimer un répertoire avant d'en avoir supprimé le contenu.

SimpleFileVisitor<Path> est la classe utilitaire qui implémente les quatre méthodes avec des comportements par défaut sensés (continuer en cas de succès, lever une exception en cas d'échec). Héritez-en et ne surchargez que les méthodes qui vous intéressent :

class LogPrinter extends SimpleFileVisitor<Path> {
  @Override public FileVisitResult visitFile(Path f, BasicFileAttributes a) {
    System.out.println(f);
    return FileVisitResult.CONTINUE;
  }
}
Files.walkFileTree(root, new LogPrinter());

C'est le visiteur minimal viable.

FileVisitResult : quatre signaux

Chaque méthode du visiteur retourne un FileVisitResult indiquant au parcoureur quoi faire ensuite :

ValeurEffet
CONTINUENormal — passer à l'entrée suivante
SKIP_SUBTREE(depuis preVisitDirectory uniquement) Ignorer ce répertoire et ses enfants entièrement
SKIP_SIBLINGSArrêter de visiter le reste du répertoire courant ; reprendre au prochain frère du parent
TERMINATEArrêter complètement le parcours

SKIP_SUBTREE est celui auquel vous ferez appel : « ne pas descendre dans .git/ ou node_modules/ ». Retournez-le depuis preVisitDirectory lorsque le nom du répertoire correspond et le parcoureur ignorera à la fois le répertoire et ses enfants :

@Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes a) {
  String name = dir.getFileName() == null ? "" : dir.getFileName().toString();
  if (name.equals(".git") || name.equals("node_modules")) {
    return FileVisitResult.SKIP_SUBTREE;
  }
  return FileVisitResult.CONTINUE;
}

TERMINATE est le signal « trouvé, arrêt » — utile quand vous cherchez le premier fichier correspondant et ne souhaitez pas parcourir le reste :

@Override public FileVisitResult visitFile(Path f, BasicFileAttributes a) {
  if (f.getFileName().toString().equals("target.txt")) {
    found = f;
    return FileVisitResult.TERMINATE;
  }
  return FileVisitResult.CONTINUE;
}

La forme Stream ne peut pas faire cela — Files.walk(...).filter(...).findFirst() court-circuite bien, mais seulement après que le parcoureur a déjà énuméré chaque entrée de répertoire dans le flux. Pour un arbre profond où la correspondance est peu profonde, walkFileTree est nettement plus rapide.

Gestion des erreurs par fichier

visitFile et preVisitDirectory ne sont appelées que lorsque le JDK a pu lire l'entrée. Si un fichier unique est illisible (permission refusée, lien symbolique brisé, situation de concurrence où il a été supprimé en plein parcours), visitFileFailed est appelée à la place avec l'exception. Par défaut, SimpleFileVisitor relève l'exception — ce qui interrompt le parcours :

@Override public FileVisitResult visitFileFailed(Path f, IOException e) throws IOException {
  throw e;                                          // default behaviour
}

Pour un parcoureur tolérant (journaliser et continuer), surchargez-la :

@Override public FileVisitResult visitFileFailed(Path f, IOException e) {
  System.err.println("skipping " + f + ": " + e.getMessage());
  return FileVisitResult.CONTINUE;
}

Files.walk(...) ne dispose pas de ce hook — il lève une UncheckedIOException depuis l'intérieur du flux dès qu'il rencontre une entrée problématique, et le flux est mort après cela. Pour les scanners longue durée sur des systèmes de fichiers que vous ne contrôlez pas entièrement, c'est une raison supplémentaire de préférer walkFileTree.

Le cas d'usage canonique : la suppression récursive

Files.delete ne fonctionne que sur les répertoires vides. Pour supprimer un arbre, vous devez d'abord supprimer les feuilles, puis les répertoires qui les contenaient. walkFileTree a la bonne forme pour cela — visitFile supprime le fichier, postVisitDirectory supprime le répertoire une fois tous ses enfants disparus :

Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
  @Override public FileVisitResult visitFile(Path f, BasicFileAttributes a) throws IOException {
    Files.delete(f);
    return FileVisitResult.CONTINUE;
  }
  @Override public FileVisitResult postVisitDirectory(Path d, IOException e) throws IOException {
    if (e != null) throw e;                          // propagate I/O failures from descent
    Files.delete(d);
    return FileVisitResult.CONTINUE;
  }
});

C'est la recette JDK pour « supprimer un arbre de répertoires ». Tout projet qui en a besoin finit par avoir une version de ce bloc de 10 lignes. Enregistrez-en une copie dans une classe utilitaire et réutilisez-la.

Liens symboliques

Par défaut, Files.walkFileTree et Files.walk ne suivent pas les liens symboliques. C'est le comportement sûr par défaut : cela prévient les boucles infinies sur un lien symbolique qui pointe vers son propre ancêtre. Pour les suivre, passez FileVisitOption.FOLLOW_LINKS :

Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS),
    Integer.MAX_VALUE, visitor);

Lorsque vous activez cette option, le parcoureur détecte les cycles pour vous — il suit les clés de répertoires visités et abandonne si le même apparaît à nouveau avec FileSystemLoopException. C'est le seul moyen de parcourir un arbre avec des liens sans écrire soi-même la détection de cycles.

Un exemple complet : impression d'arbre, saut de sous-arborescence, suppression récursive

Le programme ci-dessous construit un petit arbre de répertoires avec quelques sous-répertoires (dont un que l'on veut ignorer), des fichiers à plusieurs profondeurs, puis le parcourt de trois façons. D'abord, une impression d'arbre avec SimpleFileVisitor qui ignore .git. Ensuite, un « trouver la première correspondance » avec TERMINATE. Enfin, le schéma canonique de suppression récursive qui supprime l'ensemble de l'arbre à la fin.

java— editable, runs on the server

Ce que l'exécution nous enseigne :

  • Le hook preVisitDirectory a retourné SKIP_SUBTREE dès qu'il a vu .git. Le parcoureur n'est jamais descendu dans le répertoire ; le fichier config qu'il contient n'a jamais été visité. C'est le bon outil pour « ignorer ces répertoires conventionnels » — .git, node_modules, target, dist, et tout autre répertoire que votre projet ne veut pas parcourir. La forme Stream<Path> ne peut pas faire cela sans produire les entrées et les filtrer, ce qui coûte quand même la lecture du répertoire.
  • L'ordre des appels pour sub/ était preVisitDirectory(sub)visitFile(b.txt)preVisitDirectory(nested)visitFile(c.txt)postVisitDirectory(nested)postVisitDirectory(sub). Les hooks post* se déclenchent après que tous les descendants ont été traités — c'est le contrat en profondeur d'abord, et c'est ce qui rend le schéma de suppression récursive possible.
  • Le parcours « trouver le premier » a retourné TERMINATE depuis visitFile dès que c.txt est apparu. Tout ce qui suivait — les autres entrées dans nested/, le reste de sub/, le reste de root/ — n'a jamais été visité. Sur un petit arbre l'économie est invisible ; sur un arbre profond où la correspondance est peu profonde, c'est la différence entre O(n) et O(profondeur-de-correspondance).
  • La suppression récursive comportait deux parties. visitFile supprimait les feuilles ; postVisitDirectory supprimait les répertoires (désormais vides). L'ordre en profondeur d'abord du parcoureur garantissait que chaque enfant était visité avant le postVisitDirectory de son parent, donc Files.delete(d) voyait toujours un répertoire vide. Tenter de supprimer le répertoire dans preVisitDirectory échouerait car les enfants sont encore présents ; tenter de le supprimer avec Files.delete(root) à la fin échouerait pour la même raison. Le hook post* est toute la raison d'être de l'API visiteur.
  • Tout au long, SimpleFileVisitor était la classe de base et nous n'avons surchargé que les méthodes dont nous avions besoin. visitFileFailed a été laissé à sa valeur par défaut (lever une exception), ce qui est correct pour ces démos avec des fichiers temporaires. Pour un scanner sur un vrai système de fichiers que vous ne contrôlez pas entièrement — disons, un antivirus parcourant /, où des fichiers pourraient être supprimés sous vos pieds — surchargez visitFileFailed pour journaliser et CONTINUE.

La suite

La partie 13 se termine ici. Des fichiers ont été écrits, lus, ouverts, copiés, déplacés, supprimés, parcourus, sérialisés. Des flux ont été mis en mémoire tampon, décorés, formatés, mappés, canalisés. La partie suivante, Date et heure, aborde un problème totalement différent : représenter des instants, des durées, des dates calendaires, des fuseaux horaires, ainsi que leur formatage et leur analyse — java.time, l'API moderne qui a remplacé java.util.Date et Calendar.

Pratique

Pratique
Vous devez supprimer un arbre de répertoires contenant 50 fichiers répartis dans 10 sous-répertoires imbriqués. Quelle implémentation du hook `FileVisitor` supprime chaque répertoire uniquement après que ses enfants ont disparu ?
Vous devez supprimer un arbre de répertoires contenant 50 fichiers répartis dans 10 sous-répertoires imbriqués. Quelle implémentation du hook `FileVisitor` supprime chaque répertoire uniquement après que ses enfants ont disparu ?
Was this page helpful?