W3docs

Scorrere gli alberi di file in Java

Attraversa ricorsivamente le directory in Java con Files.walk, Files.find e l'interfaccia FileVisitor.

Il capitolo precedente si è concluso con Files.walk(dir) — la forma Stream<Path> di "dammi ogni file in questa directory." È lo strumento veloce per il caso comune. Questo capitolo tratta l'alternativa di livello più basso, Files.walkFileTree, che permette di controllare l'attraversamento in modi non consentiti dalla forma stream: gestire errori I/O per ogni file, saltare interi sottoalberi a metà percorso, eseguire codice all'uscita da una directory oltre che all'ingresso, e interrompere al primo risultato trovato.

Usa Files.walk per "elenca tutto." Usa Files.walkFileTree per "esegui qualcosa a ogni passo, con controllo sulla fase."

Tre API di scorrimento

Il catalogo, in ordine di frequenza d'uso:

APIRestituisceQuando
Files.walk(dir)Stream<Path>Più comune — filter/map/foreach su ogni voce
Files.find(dir, depth, biPredicate)Stream<Path>Come sopra, con un predicato sensibile agli attributi (isDirectory, mtime)
Files.walkFileTree(dir, visitor)Path (il punto di partenza)Servono hook pre/post-visita, gestione errori per file, o interruzione del percorso

Le prime due sono sufficienti per il 90% del codice "trovami tutti i file .log". walkFileTree è quella a cui ricorrere quando la risposta è "e poi elimina la directory" o "smetti di scorrere non appena trovi quello che cerco."

FileVisitor e SimpleFileVisitor

Files.walkFileTree accetta un FileVisitor<Path> — un'interfaccia con quattro metodi che il walker chiama in momenti specifici:

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'ordine è importante: per una directory d con figli [a, b/, c], le chiamate sono preVisitDirectory(d), visitFile(a), preVisitDirectory(b), ... postVisitDirectory(b), visitFile(c), postVisitDirectory(d). L'hook post* è ciò che rende possibile l'eliminazione ricorsiva — non puoi eliminare una directory prima di averne eliminato il contenuto.

SimpleFileVisitor<Path> è la classe helper che implementa tutti e quattro i metodi con valori predefiniti ragionevoli (continua in caso di successo, lancia eccezione in caso di fallimento). Estendila e sovrascrivi solo i metodi che ti interessano:

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());

Questo è il visitor minimo funzionante.

FileVisitResult: quattro segnali

Ogni metodo del visitor restituisce un FileVisitResult che indica al walker cosa fare in seguito:

ValoreEffetto
CONTINUENormale — passa alla voce successiva
SKIP_SUBTREE(solo da preVisitDirectory) Salta questa directory e tutti i suoi figli
SKIP_SIBLINGSSmetti di visitare il resto della directory corrente; riprendi dal prossimo fratello del genitore
TERMINATEInterrompi completamente il percorso

SKIP_SUBTREE è quello a cui ricorrerai: "non scendere in .git/ o node_modules/." Restituiscilo da preVisitDirectory quando il nome della directory corrisponde e il walker salterà sia la directory che i suoi figli:

@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 è il segnale "trovato, stop" — utile quando stai cercando il primo file corrispondente e non vuoi scorrere il resto:

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

La forma Stream non può fare questo — Files.walk(...).filter(...).findFirst() si interrompe anticipatamente, ma solo dopo che il walker ha già enumerato ogni voce della directory nello stream. Per un albero profondo dove la corrispondenza è superficiale, walkFileTree è significativamente più veloce.

Gestione degli errori per file

visitFile e preVisitDirectory vengono chiamati solo quando il JDK riesce a leggere la voce. Se un singolo file non è leggibile (permesso negato, symlink pendente, condizione di gara in cui è stato eliminato durante il percorso), viene invece chiamato visitFileFailed con l'eccezione. Per impostazione predefinita SimpleFileVisitor rilancia — il che interrompe il percorso:

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

Per un walker tollerante (registra e continua), sovrascrivilo:

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

Files.walk(...) non ha questo hook — lancia una UncheckedIOException dall'interno dello stream nel momento in cui incontra una voce problematica, e lo stream è inutilizzabile dopo. Per scanner a lungo termine su filesystem che non controlli completamente, questo è un altro motivo per preferire walkFileTree.

Il caso d'uso canonico: eliminazione ricorsiva

Files.delete funziona solo su directory vuote. Per rimuovere un albero devi eliminare prima le foglie, poi le directory che le contenevano. walkFileTree è la forma giusta per questo — visitFile elimina il file, postVisitDirectory elimina la directory una volta che tutti i suoi figli sono stati rimossi:

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;
  }
});

Questa è la ricetta JDK per "eliminare un albero di directory." Ogni codebase che ne ha bisogno finisce per avere una versione di questo blocco di 10 righe. Salvane una copia in una classe di utilità e riusala.

Per impostazione predefinita, Files.walkFileTree e Files.walk non seguono i link simbolici. È il comportamento predefinito sicuro: previene loop infiniti su un symlink che punta al proprio antenato. Per seguirli, passa FileVisitOption.FOLLOW_LINKS:

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

Quando lo abiliti, il walker rileva i cicli per te — tiene traccia delle chiavi delle directory visitate e si interrompe se la stessa riappare con FileSystemLoopException. È l'unico modo per scorrere un albero con link senza scrivere tu stesso il rilevamento dei cicli.

Un esempio pratico: stampa albero, salta sottoalbero, eliminazione ricorsiva

Il programma seguente costruisce un piccolo albero di directory con un paio di sottodirectory (una delle quali vogliamo saltare), file a più livelli di profondità, poi lo scorre in tre modi. Prima, un tree-printer con SimpleFileVisitor che salta .git. Secondo, un "trova prima corrispondenza" con TERMINATE. Terzo, il pattern canonico di eliminazione ricorsiva che rimuove l'intero albero alla fine.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • L'hook preVisitDirectory ha restituito SKIP_SUBTREE nel momento in cui ha visto .git. Il walker non è mai sceso nella directory; il file config al suo interno non è mai stato visitato. È lo strumento giusto per "ignora queste directory convenzionali" — .git, node_modules, target, dist, qualsiasi altra cosa che il tuo progetto non vuole scorrere. La forma Stream<Path> non può farlo senza produrre le voci e filtrarle, il che comporta comunque la lettura della directory.
  • L'ordine delle chiamate per sub/ è stato preVisitDirectory(sub)visitFile(b.txt)preVisitDirectory(nested)visitFile(c.txt)postVisitDirectory(nested)postVisitDirectory(sub). Gli hook post* si attivano dopo che tutti i discendenti sono stati elaborati — questo è il contratto depth-first, ed è ciò che rende possibile il pattern di eliminazione ricorsiva.
  • Il percorso "trova prima corrispondenza" ha restituito TERMINATE da visitFile nel momento in cui è apparso c.txt. Tutto ciò che seguiva — le voci rimanenti in nested/, il resto di sub/, il resto di root/ — non è mai stato visitato. Su un albero piccolo il risparmio è invisibile; su un albero profondo dove la corrispondenza è superficiale, è la differenza tra O(n) e O(profondità-corrispondenza).
  • L'eliminazione ricorsiva aveva due fasi. visitFile eliminava le foglie; postVisitDirectory eliminava le directory (ora vuote). L'ordine depth-first del walker garantiva che ogni figlio fosse visitato prima del postVisitDirectory del genitore, quindi Files.delete(d) trovava sempre una directory vuota. Tentare di eliminare la directory in preVisitDirectory fallirebbe perché i figli sono ancora presenti; tentare di eliminarla con Files.delete(root) alla fine fallirebbe per lo stesso motivo. L'hook post* è il punto centrale dell'API visitor.
  • In tutto, SimpleFileVisitor era la classe base e abbiamo sovrascritto solo i metodi necessari. visitFileFailed è rimasto al suo comportamento predefinito (lancia eccezione), che per questi demo con file temporanei va bene. Per uno scanner su un filesystem reale che non controlli completamente — ad esempio, uno scanner antivirus che scorre /, dove i file potrebbero essere eliminati mentre sei in esecuzione — sovrascrivi visitFileFailed per registrare e fare CONTINUE.

Cosa fare dopo

La parte 13 termina qui. I file sono stati scritti, letti, aperti, copiati, spostati, eliminati, scorsi, serializzati. Gli stream sono stati bufferizzati, decorati, formattati, mappati, incanalati. La prossima parte, Date e ora, affronta un problema completamente diverso: rappresentare istanti, durate, date di calendario, fusi orari, e la loro formattazione e analisi — java.time, l'API moderna che ha sostituito java.util.Date e Calendar.

Esercitazione

Pratica
Devi eliminare un albero di directory contenente 50 file in 10 sottodirectory annidate. Quale implementazione dell'hook `FileVisitor` rimuove ogni directory solo dopo che i suoi figli sono stati eliminati?
Devi eliminare un albero di directory contenente 50 file in 10 sottodirectory annidate. Quale implementazione dell'hook `FileVisitor` rimuove ogni directory solo dopo che i suoi figli sono stati eliminati?
Was this page helpful?