1. Einführung
Erinnern wir uns an ein Beispiel für einen Durchlauf mit Files.walk():
Path start = Paths.get("my-folder");
try (Stream<Path> stream = Files.walk(start)) {
stream.forEach(System.out::println);
}
Das funktioniert prima, wenn man einfach über Dateien und Ordner iterieren und etwas mit ihnen machen möchte. Aber was, wenn die Aufgabe komplexer ist?
- Ein Ordner soll rekursiv zusammen mit allen Dateien und Unterverzeichnissen gelöscht werden (nicht nur, wenn er leer ist).
- Ein Verzeichnis soll rekursiv kopiert oder verschoben werden.
- Es sollen Statistiken erhoben werden (z. B. die Gesamtgröße aller Dateien berechnen, Dateien nach Erweiterungen gruppieren).
- Fehler müssen behandelt werden (z. B. wenn auf einen Ordner kein Zugriff besteht, soll nicht das gesamte Programm abbrechen).
In solchen Fällen ist das Stream-API nicht mehr so bequem: man muss try-catch in Lambdas einbauen, die Traversierreihenfolge beachten (z. B. erst Dateien löschen und danach Ordner), und der Code wird schlecht lesbar.
Für solche Aufgaben wurde der Mechanismus zur Baum-Traversierung des Dateisystems mittels FileVisitor entwickelt.
2. Das Interface FileVisitor: wie es aufgebaut ist
Das Interface FileVisitor<T> ist ein „Ereignis-Handler“, der bei der Traversierung des Dateisystems Benachrichtigungen für jede besuchte Datei und jeden Ordner erhält.
Wenn Sie Files.walkFileTree(start, visitor) aufrufen, startet Java den Baumdurchlauf am angegebenen Pfad und ruft in jeder Phase die passenden Methoden Ihres FileVisitor auf.
Zentrale Methoden des Interfaces FileVisitor
So sieht das Interface aus:
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 — wird vor dem Betreten eines Verzeichnisses aufgerufen.
- visitFile — wird für jede Datei aufgerufen.
- visitFileFailed — wird aufgerufen, wenn auf eine Datei nicht zugegriffen werden kann.
- postVisitDirectory — wird nach dem Verlassen eines Verzeichnisses aufgerufen (also nachdem alle Dateien und Unterverzeichnisse verarbeitet wurden).
Jede dieser Methoden gibt einen Wert vom Typ FileVisitResult zurück, der bestimmt, wie der Durchlauf fortgesetzt wird:
- FileVisitResult.CONTINUE — Traversierung fortsetzen.
- FileVisitResult.SKIP_SUBTREE — das aktuelle Verzeichnis und seinen gesamten Inhalt überspringen.
- FileVisitResult.SKIP_SIBLINGS — die übrigen „Geschwister“ (Dateien und Ordner auf derselben Ebene) überspringen.
- FileVisitResult.TERMINATE — den Durchlauf vollständig beenden.
Die Klasse SimpleFileVisitor
Alle vier Methoden jedes Mal zu implementieren ist mühsam, besonders wenn man nur eine oder zwei braucht. Daher gibt es in Java den praktischen Adapter SimpleFileVisitor: er implementiert bereits alle Methoden mit einem Standardverhalten (einfach CONTINUE), und Sie können nur die benötigten überschreiben.
Beispiel:
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("Datei: " + file);
return FileVisitResult.CONTINUE;
}
}
3. Verwendung von Files.walkFileTree: ein einfaches Beispiel
Beispiel 1: Alle Dateien und Ordner ausgeben
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("Verzeichnis: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(" Datei: " + file);
return FileVisitResult.CONTINUE;
}
});
}
}
Ausgabe:
Verzeichnis: my-folder
Datei: my-folder/file1.txt
Datei: my-folder/file2.txt
Verzeichnis: my-folder/subdir
Datei: my-folder/subdir/nested.txt
Wie Sie sehen, erfolgt der Durchlauf in die Tiefe (Depth-First): Zuerst betreten wir ein Verzeichnis, dann werden dessen Dateien verarbeitet, anschließend geht es zu den Unterverzeichnissen.
4. Beispiel: rekursives Löschen eines Verzeichnisses
Eine der häufigsten Aufgaben: Einen Ordner zusammen mit seinem gesamten Inhalt löschen. Wenn Sie versuchen, einen Ordner mit Files.delete(path) zu löschen, der nicht leer ist, erhalten Sie eine Exception. Zuerst müssen alle Dateien und Unterordner gelöscht werden, erst danach der Ordner selbst.
So geht das mit 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); // Datei löschen
System.out.println("Datei gelöscht: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); // Ordner löschen, nachdem der gesamte Inhalt entfernt wurde
System.out.println("Ordner gelöscht: " + dir);
return FileVisitResult.CONTINUE;
}
});
}
}
Wichtiger Punkt:
Das Löschen von Ordnern erfolgt in postVisitDirectory, also nachdem der gesamte Inhalt gelöscht wurde. Würden wir versuchen, den Ordner zu löschen, bevor die Dateien darin entfernt sind, bekämen wir einen Fehler.
5. Beispiel: Gesamtgröße aller Dateien in einem Verzeichnis berechnen
Schreiben wir nun einen FileVisitor, der die Summe der Größen aller Dateien in einem Ordner und dessen Unterordnern berechnet.
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("Gesamtgröße: " + totalSize + " Byte");
}
}
Beachten Sie:
Wir verwenden das Feld totalSize zur Akkumulation der Größe. In realen Anwendungen sollte man statische Felder vermeiden und Variablen über Objekte weitergeben, aber der Einfachheit halber ist es hier so ausreichend.
6. Beispiel: Dateisuche per Muster (Erweiterung) mit FileVisitor
Angenommen, wir möchten alle .txt-Dateien im Verzeichnis und den Unterverzeichnissen finden und ihre Liste ausgeben.
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("Gefundene .txt-Datei: " + file);
}
return FileVisitResult.CONTINUE;
}
});
}
}
Wenn Sie die gefundenen Dateien sammeln möchten, können Sie eine Liste anlegen:
import java.util.ArrayList;
import java.util.List;
// innerhalb von 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("Insgesamt gefunden: " + txtFiles.size());
7. Fehlerbehandlung und Besonderheiten der Traversierung
Was tun, wenn der Zugriff auf eine Datei oder einen Ordner fehlt?
Beim Durchlauf begegnen manchmal Dateien oder Ordner, auf die kein Zugriff besteht (z. B. fehlende Rechte oder die Datei ist von einem anderen Prozess belegt). In diesem Fall wird die Methode visitFileFailed aufgerufen.
Beispiel:
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Zugriffsfehler auf Datei: " + file + " (" + exc + ")");
return FileVisitResult.CONTINUE; // trotz des Fehlers fortfahren
}
Wenn Sie den Durchlauf bei einem Fehler abbrechen möchten, geben Sie TERMINATE zurück.
Wie lässt sich ein Ordner komplett überspringen?
Wenn Sie ein bestimmtes Verzeichnis nicht betreten möchten (z. B. der Ordner .git oder node_modules), können Sie in der Methode preVisitDirectory SKIP_SUBTREE zurückgeben:
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().equals("node_modules")) {
return FileVisitResult.SKIP_SUBTREE; // dieses Verzeichnis und seine Unterverzeichnisse nicht betreten
}
return FileVisitResult.CONTINUE;
}
8. Praxis: einen eigenen FileVisitor implementieren
Implementieren wir einen FileVisitor, der:
- alle Dateien mit der Erweiterung .java im Verzeichnis und in Unterverzeichnissen findet,
- ihre Anzahl ermittelt,
- und ihre Gesamtgröße berechnet.
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"); // zum Beispiel der Quellcode des Projekts
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("Gefundene .java-Datei: " + file);
}
return FileVisitResult.CONTINUE;
}
}
JavaFileVisitor visitor = new JavaFileVisitor();
Files.walkFileTree(start, visitor);
System.out.println("Gesamtzahl der .java-Dateien: " + visitor.count);
System.out.println("Gesamtgröße: " + visitor.totalSize + " Byte");
}
}
9. Typische Fehler beim Einsatz von FileVisitor
Fehler Nr. 1: Versuch, ein Verzeichnis zu löschen, bevor die Dateien darin gelöscht wurden. Wenn Sie Files.delete(dir) in preVisitDirectory aufrufen, erhalten Sie eine Exception — zuerst müssen alle Dateien und Unterordner gelöscht werden, erst danach das Verzeichnis (tun Sie das in postVisitDirectory).
Fehler Nr. 2: Zugriffsfehler nicht behandelt. Wenn visitFileFailed nicht überschrieben wird, kann das Programm beim Treffen auf eine geschützte Datei unerwartet beendet werden. Besser ist es, den Fehler explizit auszugeben und den Durchlauf fortzusetzen.
Fehler Nr. 3: Erwartung, dass die Operation „Datei verbergen“ auf allen Betriebssystemen gleich funktioniert. Unter Linux und macOS gilt eine Datei als versteckt, wenn sie mit einem Punkt beginnt (.gitignore), unter Windows steuert dies hingegen ein spezielles Attribut. Verwechseln Sie diese Ansätze nicht.
Fehler Nr. 4: Statische Felder zur Ergebnissammlung in nebenläufigen Anwendungen verwenden. Wenn Sie mehrere Durchläufe parallel starten, führen statische Felder zu Verwirrung. Verwenden Sie besser Instanzfelder (oder einen lokalen Klasse, wie im Beispiel oben).
Fehler Nr. 5: Ressourcen nicht schließen, wenn innerhalb von FileVisitor mit Streams gearbeitet wird. Wenn Ihr FileVisitor Dateien liest/schreibt, verwenden Sie try-with-resources, um Lecks zu vermeiden.
GO TO FULL VERSION