1. Introduzione
Ripassiamo un esempio di attraversamento con Files.walk():
Path start = Paths.get("my-folder");
try (Stream<Path> stream = Files.walk(start)) {
stream.forEach(System.out::println);
}
Questo funziona benissimo se bisogna semplicemente scorrere file e cartelle e fare qualcosa con essi. Ma cosa succede se il compito è più complesso?
- È necessario eliminare ricorsivamente una cartella insieme a tutti i file e alle sottodirectory (non solo quando è vuota).
- È necessario copiare o spostare ricorsivamente una directory.
- È necessario raccogliere statistiche (ad esempio calcolare la dimensione totale di tutti i file, raggruppare i file per estensione).
- È necessario gestire gli errori (ad esempio, se non si ha accesso a una cartella, non vogliamo che tutto vada in crash).
In questi casi lo Stream API non è così comodo: tocca incastrare try-catch nelle lambda, controllare l'ordine di attraversamento (ad esempio, prima eliminare i file e poi le cartelle) e il codice diventa poco leggibile.
Per questi compiti è stato pensato il meccanismo di attraversamento dell'albero del file system tramite FileVisitor.
2. Interfaccia FileVisitor: com'è strutturata
L'interfaccia FileVisitor<T> è una sorta di «gestore di eventi» che riceve notifiche su ogni file e cartella visitati durante l'attraversamento del file system.
Quando si chiama Files.walkFileTree(start, visitor), Java inizia l'attraversamento dell'albero a partire dal percorso indicato e a ogni fase invoca il metodo corrispondente del tuo FileVisitor.
Metodi principali dell'interfaccia FileVisitor
Ecco come appare l'interfaccia:
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 — viene chiamato prima di entrare nella directory.
- visitFile — viene chiamato per ogni file.
- visitFileFailed — viene chiamato se non è stato possibile accedere al file.
- postVisitDirectory — viene chiamato dopo l'uscita dalla directory (cioè dopo aver elaborato tutti i suoi file e sottodirectory).
Ognuno di questi metodi restituisce un valore di tipo FileVisitResult, che determina come continuare l'attraversamento:
- FileVisitResult.CONTINUE — continuare l'attraversamento.
- FileVisitResult.SKIP_SUBTREE — saltare la directory corrente e tutto ciò che contiene.
- FileVisitResult.SKIP_SIBLINGS — saltare gli altri «fratelli» (file e cartelle allo stesso livello).
- FileVisitResult.TERMINATE — interrompere completamente l'attraversamento.
Classe SimpleFileVisitor
Implementare ogni volta tutti e quattro i metodi è faticoso, soprattutto se te ne servono solo uno o due. Per questo in Java esiste un comodo adapter SimpleFileVisitor: implementa già tutti i metodi con un comportamento «predefinito» (semplicemente CONTINUE) e puoi sovrascrivere solo quelli necessari.
Esempio:
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("File: " + file);
return FileVisitResult.CONTINUE;
}
}
3. Uso di Files.walkFileTree: esempio di base
Esempio 1: stampiamo tutti i file e le cartelle
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("Cartella: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(" File: " + file);
return FileVisitResult.CONTINUE;
}
});
}
}
Output:
Cartella: my-folder
File: my-folder/file1.txt
File: my-folder/file2.txt
Cartella: my-folder/subdir
File: my-folder/subdir/nested.txt
Come si vede, l'attraversamento avviene «in profondità»: prima si entra nella cartella, poi si elaborano i suoi file, quindi si passa alle sottocartelle.
4. Esempio: eliminazione ricorsiva di una directory
Uno dei compiti più frequenti: eliminare una cartella insieme a tutto il suo contenuto. Se si prova a eliminare una cartella con Files.delete(path) e non è vuota, si ottiene un'eccezione. Bisogna prima eliminare tutti i file e le sottocartelle, e solo dopo la cartella stessa.
Ecco come farlo 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); // eliminiamo il file
System.out.println("File eliminato: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); // eliminiamo la cartella dopo aver eliminato tutto al suo interno
System.out.println("Cartella eliminata: " + dir);
return FileVisitResult.CONTINUE;
}
});
}
}
Punto importante:
L'eliminazione delle cartelle avviene in postVisitDirectory, cioè dopo aver eliminato tutto il contenuto. Se provassimo a eliminare la cartella prima di eliminare i file al suo interno, otterremmo un errore.
5. Esempio: calcolo della dimensione totale di tutti i file in una directory
Scriviamo ora un FileVisitor che calcoli la dimensione complessiva di tutti i file nella cartella e nelle sue sottocartelle.
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("Dimensione totale: " + totalSize + " byte");
}
}
Nota:
Usiamo il campo totalSize per accumulare la dimensione. Nelle applicazioni reali è meglio evitare campi statici e passare le variabili tramite oggetti, ma per semplicità dell'esempio va bene così.
6. Esempio: ricerca di file per pattern (estensione) con FileVisitor
Supponiamo di dover trovare tutti i file .txt nella directory e nelle sottodirectory e stamparne l'elenco.
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("Trovato file .txt: " + file);
}
return FileVisitResult.CONTINUE;
}
});
}
}
Se vuoi raccogliere l'elenco dei file trovati, puoi creare una lista:
import java.util.ArrayList;
import java.util.List;
// all'interno di 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("Trovati in totale: " + txtFiles.size());
7. Gestione degli errori e peculiarità dell'attraversamento
Cosa fare se non si ha accesso a un file o a una cartella?
A volte durante l'attraversamento si incontrano file o cartelle a cui non si ha accesso (ad esempio, se non si hanno i permessi o il file è occupato da un altro processo). In questo caso verrà chiamato il metodo visitFileFailed.
Esempio:
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Errore di accesso al file: " + file + " (" + exc + ")");
return FileVisitResult.CONTINUE; // continuiamo l'attraversamento nonostante l'errore
}
Se desideri interrompere l'attraversamento in caso di errore, restituisci TERMINATE.
Come saltare un'intera cartella?
Se non vuoi entrare in una determinata cartella (ad esempio la cartella .git o node_modules), puoi restituire SKIP_SUBTREE nel metodo preVisitDirectory:
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().equals("node_modules")) {
return FileVisitResult.SKIP_SUBTREE; // non entrare in questa cartella e nelle sue sottocartelle
}
return FileVisitResult.CONTINUE;
}
8. Pratica: implementiamo un FileVisitor personalizzato
Implementiamo un FileVisitor che:
- Trova tutti i file con estensione .java nella directory e nelle sottodirectory,
- Ne conta il numero,
- Calcola la dimensione complessiva di tali file.
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"); // ad esempio, il codice sorgente del progetto
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("Trovato file .java: " + file);
}
return FileVisitResult.CONTINUE;
}
}
JavaFileVisitor visitor = new JavaFileVisitor();
Files.walkFileTree(start, visitor);
System.out.println("Totale file .java: " + visitor.count);
System.out.println("Dimensione totale: " + visitor.totalSize + " byte");
}
}
9. Errori tipici nell'uso di FileVisitor
Errore n. 1: tentare di eliminare una directory prima di eliminare i file al suo interno. Se chiami Files.delete(dir) in preVisitDirectory, otterrai un'eccezione: prima bisogna eliminare tutti i file e le sottocartelle, poi la directory stessa (fallo in postVisitDirectory).
Errore n. 2: dimenticare di gestire gli errori di accesso. Se non sovrascrivi visitFileFailed, il programma può terminare inaspettatamente quando incontra un file protetto. È meglio stampare esplicitamente l'errore e continuare l'attraversamento.
Errore n. 3: aspettarsi che l'operazione “nascondi file” funzioni allo stesso modo su tutti i sistemi operativi. Su Linux e macOS è considerato nascosto un file che inizia con un punto (.gitignore), mentre su Windows è necessario impostare un attributo specifico. Non confondere questi approcci.
Errore n. 4: usare campi statici per accumulare il risultato in applicazioni multithread. Se esegui più attraversamenti in parallelo, i campi statici porteranno a confusione. È meglio usare campi di istanza (o di una classe locale, come nell'esempio sopra).
Errore n. 5: dimenticare di chiudere le risorse quando si lavora con stream all'interno di FileVisitor. Se il tuo FileVisitor legge/scrive file, usa try-with-resources per evitare perdite.
GO TO FULL VERSION