
Mit der Klasse Files kannst du den Inhalt eines Verzeichnisses auslesen: Files.list() liefert die Einträge eines einzelnen Verzeichnisses, Files.walk(), Files.walkFileTree() und Files.find() durchlaufen einen ganzen Verzeichnisbaum. Alle vier Methoden liefern die Einträge als Path-Objekte, mit denen du direkt weiterarbeiten kannst – filtern, lesen, kopieren, löschen.
Ein einzelnes Verzeichnis liest du in den meisten Fällen so aus:
try (Stream<Path> entries = Files.list(Path.of("files/project"))) {
entries.forEach(System.out::println);
}
files/project/pom.xml
files/project/docs
files/project/README.md
files/project/src
Files.list() öffnet das Verzeichnis und liefert seine Einträge als Stream<Path> – Dateien und Unterverzeichnisse, aber nicht deren Inhalt (also nicht die Dateien und Verzeichnisse in den Unterverzeichnissen). Der Stream hält das Verzeichnis offen, bis er geschlossen wird; deshalb gehört er in ein Try-with-resources.
Files.walkFileTree() gibt es seit Java 7, die drei Stream-Methoden list(), walk() und find() seit Java 8. Die Methoden list() und listFiles() der Klasse File sind älter, und sie haben eine Eigenschaft, die ich am Ende des Artikels zeige: Im Fehlerfall liefern sie null.
In diesem Artikel erfährst du,
- wie du mit
Files.list()ein Verzeichnis ausliest und in welcher Reihenfolge die Einträge kommen, - wie du die Einträge nach einem Glob-Muster wie
*.{md,xml}filterst, - wie du einen Verzeichnisbaum rekursiv durchsuchst – mit
Files.list()und eigener Rekursion, mitFiles.walk(),Files.walkFileTree()undFiles.find(), - in welcher Reihenfolge
walkFileTree()die vier Methoden einesFileVisitoraufruft und wasSKIP_SUBTREE,SKIP_SIBLINGSundTERMINATEbewirken, - wie du die Suchtiefe begrenzt und symbolischen Links folgst,
- was passiert, wenn ein Unterverzeichnis nicht lesbar ist, und bei welcher Variante die Suche trotzdem weiterläuft,
- welche der vier Varianten ich wann empfehle,
- warum du
File.listFiles()nur noch in altem Code findest.
Die Beispiele in diesem Artikel
Alle Beispiele lesen das Verzeichnis files/project/ aus dem Beispiel-Repository. Es enthält ein kleines Maven-Projekt mit fünf Dateien in drei Unterverzeichnissen:
project/
├── README.md
├── pom.xml
├── docs/
│ └── setup.md
└── src/
├── Main.java
└── util/
└── Strings.java
Den vollständigen Code findest du im GitHub-Repository java-file-io, im Package eu.happycoders.listing. Jeder Abschnitt hat dort eine eigene Klasse mit main()-Methode, die die gezeigten Ausgaben erzeugt. Starte die Klassen im Hauptverzeichnis des Repositorys; Verzeichnisse, die von einigen Beispielen selbst angelegt werden, landen unter target/.
Die Klassen im Repository verwenden Sprachmittel aus Java 25 und laufen deshalb erst ab dieser Version. Die Ausschnitte im Artikel laufen ab Java 11 – der Version, die Path.of() und Files.writeString() eingeführt hat. Die Ausgaben stammen von macOS; wo Linux oder Windows abweichen, steht es im Text.
Ein Verzeichnis auslesen mit Files.list()
Files.list() liefert die Einträge eines Verzeichnisses als Stream<Path>. Jeder Eintrag ist der Pfad des Verzeichnisses, ergänzt um den Namen des Eintrags – aus files/project und pom.xml wird files/project/pom.xml. Die Einträge . und .., die das Dateisystem für das Verzeichnis selbst und sein übergeordnetes Verzeichnis führt, sind nicht enthalten.
Die Methode ist nicht rekursiv. Das zeigt der Aufruf auf dem Unterverzeichnis src:
Path project = Path.of("files/project");
try (Stream<Path> entries = Files.list(project.resolve("src"))) {
entries.forEach(System.out::println);
}
files/project/src/util
files/project/src/Main.java
Das Unterverzeichnis util erscheint als Eintrag, die Datei Strings.java darin nicht. Wie du auch die Einträge der Unterverzeichnisse bekommst, zeigt der Abschnitt Einen Verzeichnisbaum rekursiv durchsuchen.
Die Reihenfolge der Einträge
In welcher Reihenfolge die Einträge kommen, legt das Dateisystem fest. In der Ausgabe aus der Einleitung steht pom.xml vor docs und README.md – das ist die Reihenfolge, in der APFS, das Dateisystem meines Macs, die Einträge liefert. Ein anderes Dateisystem liefert eine andere Reihenfolge. Die kann zufällig alphabetisch sein; verlassen kannst du dich darauf nicht.
Brauchst du eine sortierte Liste, musst du den Stream selbst sortieren:
try (Stream<Path> entries = Files.list(project)) {
entries.sorted().forEach(System.out::println);
}
files/project/README.md
files/project/docs
files/project/pom.xml
files/project/src
Path implementiert Comparable, deshalb reicht sorted() ohne Argument. Unter macOS und Linux vergleicht die Sortierung die Pfade Zeichen für Zeichen nach dem Unicode-Wert des Zeichens – Großbuchstaben (A = 65 bis Z = 90) kommen vor Kleinbuchstaben (a = 97 bis z = 122), daher README.md vor docs. Unter Windows ignoriert die Sortierung Groß- und Kleinschreibung; dort steht docs vor README.md.
Nur Dateien oder nur Verzeichnisse
Da Files.list() einen Stream liefert, kannst du die Einträge mit den Operationen der Stream-API filtern. Das folgende Beispiel gibt nur die Unterverzeichnisse aus:
try (Stream<Path> entries = Files.list(project)) {
entries.filter(Files::isDirectory).forEach(System.out::println);
}
files/project/docs
files/project/src
Für Dateien nimmst du Files::isRegularFile. Ein Eintrag, für den beide Methoden false liefern, ist entweder ein symbolischer Link, den du mit Files.isSymbolicLink() erkennst, oder vom Typ „other“, z. B. eine Named Pipe, eine Gerätedatei oder ein Socket.
Den Stream schließen
Der Stream aus Files.list() hält ein offenes Verzeichnis – ein Datei-Handle des Betriebssystems –, bis du ihn schließt. Jedes Betriebssystem begrenzt die Anzahl der Handles pro Prozess. Ein Programm, das Streams aus Files.list() nicht schließt, verbraucht die Handles nach und nach, bis das nächste Öffnen einer Datei fehlschlägt. Das folgende Programm öffnet Streams, ohne sie zu schließen, so lange, bis das passiert:
List<Stream<Path>> open = new ArrayList<>();
try {
while (true) {
open.add(Files.list(project)); // wird nie geschlossen
}
} catch (IOException e) {
System.out.println(open.size() + " streams open, then: " + e);
} finally {
for (Stream<Path> stream : open) {
stream.close();
}
}
122878 streams open, then: java.nio.file.FileSystemException: files/project: Too many open files
Auf meinem Mac ist nach 122.878 offenen Streams Schluss; wie viele es bei dir sind, hängt von Betriebssystem und Konfiguration ab. In einem Server beispielsweise, der pro Anfrage mit Files.list() ein Verzeichnis auflistet und den Stream nicht wieder schließt, ist diese Grenze irgendwann erreicht – und dann scheitert nicht nur das nächste Files.list(), sondern jedes Öffnen einer Datei oder Netzwerkverbindung im selben Prozess. Denn unter macOS und Linux zählt auch eine Netzwerkverbindung als offene Datei.
Deshalb steht der Stream in allen Beispielen dieses Artikels in einem Try-with-resources. Das gilt für die Streams aus Files.walk() und Files.find() genauso – die halten sogar alle Verzeichnisse auf dem Weg vom Startverzeichnis bis zum aktuellen Eintrag offen.
NotDirectoryException und NoSuchFileException
Ist der Pfad kein Verzeichnis, sondern eine Datei, wirft Files.list() eine NotDirectoryException. Gibt es den Pfad nicht, wirft es eine NoSuchFileException:
try (Stream<Path> entries = Files.list(project.resolve("pom.xml"))) {
entries.forEach(System.out::println);
} catch (NotDirectoryException e) {
System.out.println(e);
}
try (Stream<Path> entries = Files.list(project.resolve("test"))) {
entries.forEach(System.out::println);
} catch (NoSuchFileException e) {
System.out.println(e);
}
java.nio.file.NotDirectoryException: files/project/pom.xml
java.nio.file.NoSuchFileException: files/project/test
Beide sind Unterklassen von IOException, und beide werden schon beim Aufruf von Files.list() geworfen – nicht erst beim Lesen des Streams.
Nach Dateinamen filtern mit Files.newDirectoryStream() und Glob-Mustern
Willst du nur Einträge mit bestimmten Namen, etwa alle Markdown-Dateien, gibt es einen Weg aus Java 7, der ohne Stream-API auskommt: Files.newDirectoryStream() mit einem Glob-Muster als zweitem Argument. Ein Glob ist ein Muster, wie du es aus der Shell kennst: * steht für beliebig viele Zeichen innerhalb eines Namens, ? für ein einzelnes Zeichen, und in geschweiften Klammern stehen Alternativen.
try (DirectoryStream<Path> entries =
Files.newDirectoryStream(project, "*.{md,xml}")) {
for (Path entry : entries) {
System.out.println(entry);
}
}
files/project/pom.xml
files/project/README.md
DirectoryStream ist kein java.util.stream.Stream, sondern ein Iterable, das du mit einer For-each-Schleife durchläufst – und das du ebenfalls schließen musst, denn auch der DirectoryStream hält das Verzeichnis offen. So wie einen java.util.stream.Stream darfst du auch einen DirectoryStream nur einmal durchlaufen.
Welche Muster ein Glob erlaubt, steht in der Javadoc von FileSystem.getPathMatcher(). Dasselbe Muster kannst du auch auf die Treffer einer rekursiven Suche anwenden; wie, zeigt der Abschnitt Nach Mustern suchen mit PathMatcher.
Einen Verzeichnisbaum rekursiv durchsuchen
Oft brauchst du nicht nur die Einträge eines einzelnen Verzeichnisses, sondern alle Dateien unterhalb eines Verzeichnisses, in beliebiger Tiefe – z. B. alle .java-Dateien eines Projekts. Dafür gibt es vier Wege. Ich zeige sie am selben Beispiel, damit du sie vergleichen kannst.
Mit Files.list() und eigener Rekursion
Der erste Weg baut die Rekursion selbst: Die Methode findJavaFiles() listet ein Verzeichnis mit Files.list() auf, gibt jede .java-Datei aus und ruft sich für jedes Unterverzeichnis selbst auf.
private static void findJavaFiles(Path dir) throws IOException {
try (Stream<Path> entries = Files.list(dir)) {
entries.forEach(entry -> {
if (Files.isRegularFile(entry) && entry.toString().endsWith(".java")) {
System.out.println(entry);
}
if (Files.isDirectory(entry)) {
try {
findJavaFiles(entry);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
});
}
}
files/project/src/util/Strings.java
files/project/src/Main.java
Das Try-catch im Lambda ist nötig, weil der Consumer von forEach() keine checked Exception werfen darf. Die IOException aus dem rekursiven Aufruf wird deshalb in eine UncheckedIOException verpackt.
Dieser Weg ist der längste, aber du behältst die Kontrolle über jedes Verzeichnis: Du entscheidest pro Verzeichnis, ob du hineingehst, und du kannst einen Fehler für ein einzelnes Verzeichnis abfangen und weitersuchen. Was das bringt, zeigt der Abschnitt Gesperrte Verzeichnisse: AccessDeniedException.
Mit Files.walk()
Files.walk() übernimmt die Rekursion: Es liefert einen Stream mit dem Startverzeichnis und allen Einträgen darunter, in beliebiger Tiefe.
try (Stream<Path> paths = Files.walk(project)) {
paths.forEach(System.out::println);
}
files/project
files/project/pom.xml
files/project/docs
files/project/docs/setup.md
files/project/README.md
files/project/src
files/project/src/util
files/project/src/util/Strings.java
files/project/src/Main.java
Das erste Element ist das Startverzeichnis selbst. Danach kommen die Einträge in Tiefensuche: Auf ein Verzeichnis folgen sofort seine Einträge und deren Einträge, und erst danach geht es mit dem nächsten Eintrag auf derselben Ebene weiter – auf docs folgt docs/setup.md, dann erst README.md. Innerhalb eines Verzeichnisses gilt wieder die Reihenfolge des Dateisystems.
Die Suche nach .java-Dateien wird damit zu einer Stream-Pipeline:
try (Stream<Path> paths = Files.walk(project)) {
paths
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".java"))
.forEach(System.out::println);
}
files/project/src/util/Strings.java
files/project/src/Main.java
Der Stream liest die Verzeichnisse erst, während die Pipeline läuft, nicht beim Aufruf von walk(). Eine IOException dabei – beispielsweise durch ein Verzeichnis, das sich nicht öffnen lässt – verpackt er in eine UncheckedIOException und wirft diese aus der terminalen Operation, hier forEach(). Damit endet die gesamte Suche; die Treffer vor dem Fehler sind ausgegeben, alle danach fehlen.
Die Suchtiefe begrenzen
Mit einem zweiten Argument begrenzt du, wie viele Verzeichnisebenen Files.walk() hinabsteigt. Die Tiefe 1 liefert das Startverzeichnis und seine direkten Einträge:
try (Stream<Path> paths = Files.walk(project, 1)) {
paths.forEach(System.out::println);
}
files/project
files/project/pom.xml
files/project/docs
files/project/README.md
files/project/src
Das ist die Ausgabe von Files.list(), ergänzt um das Startverzeichnis. Mit der Tiefe 0 liefert walk() nur das Startverzeichnis; ohne Angabe gilt Integer.MAX_VALUE – damit liefert walk() alle Ebenen des Baums, egal wie tief er ist.
Symbolischen Links folgen
Symbolischen Links folgt Files.walk() standardmäßig nicht: Ein Link auf ein Verzeichnis erscheint als Eintrag, sein Inhalt nicht. Das folgende Beispiel legt unter target/links/ ein Verzeichnis data mit einer Datei und daneben den symbolischen Link shortcut an, der auf data zeigt – und durchläuft den Baum einmal ohne und einmal mit der Option FileVisitOption.FOLLOW_LINKS:
Path links = Path.of("target/links");
Files.createDirectories(links.resolve("data"));
Files.writeString(links.resolve("data/a.txt"), "a\n");
Files.createSymbolicLink(links.resolve("shortcut"), Path.of("data"));
System.out.println("-- ohne FOLLOW_LINKS");
try (Stream<Path> paths = Files.walk(links)) {
paths.forEach(System.out::println);
}
System.out.println();
System.out.println("-- mit FOLLOW_LINKS");
try (Stream<Path> paths = Files.walk(links, FOLLOW_LINKS)) {
paths.forEach(System.out::println);
}
-- ohne FOLLOW_LINKS
target/links
target/links/shortcut
target/links/data
target/links/data/a.txt
-- mit FOLLOW_LINKS
target/links
target/links/shortcut
target/links/shortcut/a.txt
target/links/data
target/links/data/a.txt
Mit FOLLOW_LINKS erscheint die Datei a.txt zweimal: einmal über den gefolgten Link zum shortcut-Verzeichnis und einmal direkt.
Ein Link, der auf ein übergeordnetes Verzeichnis zeigt, würde die Suche ohne Gegenmaßnahme endlos laufen lassen. Deshalb merkt sich walk() mit dieser Option jedes besuchte Verzeichnis und meldet einen solchen Zyklus als FileSystemLoopException. Das zeigt der nächste Aufruf, nachdem in data ein Link loop auf das übergeordnete Verzeichnis angelegt wurde:
Files.createSymbolicLink(links.resolve("data/loop"), Path.of(".."));
try (Stream<Path> paths = Files.walk(links, FOLLOW_LINKS)) {
paths.forEach(System.out::println);
} catch (UncheckedIOException e) {
System.out.println(e.getCause());
}
target/links
target/links/shortcut
target/links/shortcut/a.txt
java.nio.file.FileSystemLoopException: target/links/shortcut/loop
Die Exception kommt, wie jede IOException aus dem Stream, verpackt in eine UncheckedIOException. Die Ausgabe zeigt die Ursache, die getCause() liefert.
Mit Files.walkFileTree()
Der dritte Weg ist Files.walkFileTree(), der einzige der vier Wege ohne Stream. Die Methode implementiert das Visitor-Pattern: Sie durchläuft den Verzeichnisbaum und ruft für jeden Eintrag eine Methode einer FileVisitor-Implementierung auf, die du ihr übergibst. Die Klasse SimpleFileVisitor implementiert alle vier Methoden des Interfaces, sodass du nur die überschreiben musst, die du brauchst – für die Suche nach .java-Dateien ist das visitFile():
Files.walkFileTree(project, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (attrs.isRegularFile() && file.toString().endsWith(".java")) {
System.out.println(file);
}
return FileVisitResult.CONTINUE;
}
});
files/project/src/util/Strings.java
files/project/src/Main.java
Der zweite Parameter von visitFile() sind die BasicFileAttributes des Eintrags – Typ, Größe, Zeitstempel. walkFileTree() liest sie für jeden Eintrag, um zu entscheiden, ob es sich um ein Verzeichnis handelt, und gibt sie an den Visitor weiter. attrs.isRegularFile() kostet deshalb keinen weiteren Zugriff auf das Dateisystem; Files.isRegularFile(file) hingegen würde die Attribute ein zweites Mal lesen.
Die IOException eines Visitors musst du nicht verpacken: Alle vier Methoden des FileVisitor dürfen sie werfen, und walkFileTree() reicht sie an den Aufrufer weiter.
Die Aufrufreihenfolge der vier Methoden
FileVisitor hat vier Methoden, und walkFileTree() ruft sie in einer festen Reihenfolge auf: Für jedes Verzeichnis kommt zuerst preVisitDirectory(), dann für jeden Eintrag visitFile() – bei einem Unterverzeichnis stattdessen wieder dieselbe Folge – und zum Schluss postVisitDirectory(). Kann walkFileTree() einen Eintrag nicht lesen, ruft es visitFileFailed() auf.
Das folgende Diagramm zeigt die Aufrufe für ein kleineres Verzeichnis projekt mit der Datei README.md und dem Unterverzeichnis src, in dem Main.java liegt:
projekt/
├── README.md
└── src/
└── Main.java
Im Diagramm sind Aufrufe für Verzeichnisse blau umrandet, Aufrufe für Dateien grau. In welcher Reihenfolge die Einträge innerhalb eines Verzeichnisses kommen – hier README.md vor src –, legt das Dateisystem fest. Die eckige Klammer rechts im Diagramm zeigt, welche Aufrufe entfallen, wenn preVisitDirectory() für src den Rückgabewert SKIP_SUBTREE liefert; die Rückgabewerte erklärt der nächste Abschnitt.
Für das vollständige Beispielverzeichnis dieses Artikels (nicht das kleinere aus dem Diagramm) gibt der folgende Visitor jeden Aufruf nummeriert aus. Er überschreibt die drei Methoden, die bei lesbaren Verzeichnissen aufgerufen werden:
static class LoggingVisitor extends SimpleFileVisitor<Path> {
private int step;
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println(++step + ". preVisitDirectory(" + dir + ")");
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(++step + ". visitFile(" + file + ")");
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
System.out.println(++step + ". postVisitDirectory(" + dir + ")");
return FileVisitResult.CONTINUE;
}
}
Files.walkFileTree(project, new LoggingVisitor());
1. preVisitDirectory(files/project)
2. visitFile(files/project/pom.xml)
3. preVisitDirectory(files/project/docs)
4. visitFile(files/project/docs/setup.md)
5. postVisitDirectory(files/project/docs)
6. visitFile(files/project/README.md)
7. preVisitDirectory(files/project/src)
8. preVisitDirectory(files/project/src/util)
9. visitFile(files/project/src/util/Strings.java)
10. postVisitDirectory(files/project/src/util)
11. visitFile(files/project/src/Main.java)
12. postVisitDirectory(files/project/src)
13. postVisitDirectory(files/project)
Beachte die Schritt-Paare 7/12 und 1/13: postVisitDirectory() für src kommt erst, nachdem auch das Unterverzeichnis util vollständig besucht ist – und postVisitDirectory() für project kommt erst, nachdem auch die Unterverzeichnisse docs und src vollständig besucht sind.
Dieses Paar aus preVisitDirectory() und postVisitDirectory() macht walkFileTree() zum Werkzeug für Operationen auf ganzen Verzeichnisbäumen: In preVisitDirectory() legst du beim Kopieren das Zielverzeichnis an, bevor die Dateien kommen, und in postVisitDirectory() entfernst du beim Löschen das Verzeichnis, nachdem es leer ist. Beides zeigt der Artikel über das Erstellen, Kopieren, Verschieben und Löschen von Dateien und Verzeichnissen.
Der Parameter exc von postVisitDirectory() ist null, wenn alle Einträge des Verzeichnisses besucht wurden, und sonst die IOException, die das Durchlaufen abgebrochen hat.
CONTINUE, SKIP_SUBTREE, SKIP_SIBLINGS und TERMINATE
Jede Methode des FileVisitor liefert ein FileVisitResult, mit dem du den weiteren Durchlauf steuern kannst:
CONTINUE–walkFileTree()durchläuft den Baum weiter; auspreVisitDirectory()zurückgegeben heißt das: auch die Einträge dieses Verzeichnisses.SKIP_SUBTREE– überspringt die Einträge des Verzeichnisses und den Aufruf vonpostVisitDirectory(). Dieser Rückgabewert hat nur inpreVisitDirectory()eine Wirkung; aus den anderen Methoden zurückgegeben, wirkt er wieCONTINUE.SKIP_SIBLINGS– überspringt alle weiteren Einträge des Verzeichnisses, in dem der aktuelle Eintrag liegt. AuspreVisitDirectory()zurückgegeben, überspringt es zusätzlich die Einträge dieses Verzeichnisses und den Aufruf vonpostVisitDirectory()– wieSKIP_SUBTREE.TERMINATE– beendetwalkFileTree()sofort; es folgt kein weiterer Aufruf, auch keinpostVisitDirectory()für die noch offenen Verzeichnisse.
Mit SKIP_SUBTREE lässt du Verzeichnisse aus, die dich nicht interessieren – z. B. target, node_modules oder .git. Das folgende Beispiel überspringt src:
Files.walkFileTree(project, new LoggingVisitor() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
super.preVisitDirectory(dir, attrs);
return dir.endsWith("src")
? FileVisitResult.SKIP_SUBTREE
: FileVisitResult.CONTINUE;
}
});
1. preVisitDirectory(files/project)
2. visitFile(files/project/pom.xml)
3. preVisitDirectory(files/project/docs)
4. visitFile(files/project/docs/setup.md)
5. postVisitDirectory(files/project/docs)
6. visitFile(files/project/README.md)
7. preVisitDirectory(files/project/src)
8. postVisitDirectory(files/project)
Nach preVisitDirectory() für src in Schritt 7 kommt direkt postVisitDirectory() für project: Die vier Aufrufe für den Inhalt von src und das postVisitDirectory() für src entfallen. Path.endsWith("src") prüft übrigens, ob der letzte Pfadbestandteil src heißt – nicht, ob der Pfad als String auf src endet.
Mit TERMINATE beendest du die Suche beim ersten Treffer. Das folgende Beispiel hört bei der ersten .java-Datei auf:
Files.walkFileTree(project, new LoggingVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
super.visitFile(file, attrs);
return file.toString().endsWith(".java")
? FileVisitResult.TERMINATE
: FileVisitResult.CONTINUE;
}
});
1. preVisitDirectory(files/project)
2. visitFile(files/project/pom.xml)
3. preVisitDirectory(files/project/docs)
4. visitFile(files/project/docs/setup.md)
5. postVisitDirectory(files/project/docs)
6. visitFile(files/project/README.md)
7. preVisitDirectory(files/project/src)
8. preVisitDirectory(files/project/src/util)
9. visitFile(files/project/src/util/Strings.java)
Nach Schritt 9 ist Schluss – ohne postVisitDirectory() für util, src und project.
Die Suchtiefe begrenzt du bei walkFileTree() mit der Variante mit vier Parametern: Files.walkFileTree(start, options, maxDepth, visitor). Der zweite Parameter ist ein Set<FileVisitOption>, mit dem du wie bei walk() symbolischen Links folgen kannst; die Zweiparameter-Variante aus den Beispielen oben folgt keinen Links und durchläuft alle Ebenen.
Mit Files.find()
Der vierte Weg ist Files.find(). Die Methode durchläuft den Baum wie walk(), nimmt aber zusätzlich einen Matcher entgegen: ein BiPredicate, das für jeden Eintrag dessen Path und BasicFileAttributes bekommt und mit dem Rückgabewert bestimmt, ob der Eintrag in den Stream kommt oder nicht. Die maximale Tiefe ist bei find() ein Pflichtparameter:
try (Stream<Path> matches = Files.find(project, Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile() && path.toString().endsWith(".java"))) {
matches.forEach(System.out::println);
}
files/project/src/util/Strings.java
files/project/src/Main.java
Der Matcher entscheidet nur, welche Einträge in den Stream kommen, nicht, in welche Verzeichnisse find() hinabsteigt. Das zeigt schon dieses Beispiel: Für src und util liefert der Matcher false, und trotzdem findet find() die Dateien darin. Ganze Teilbäume überspringen kannst du nur mit walkFileTree() und SKIP_SUBTREE.
Der Unterschied zu walk() mit filter() liegt in den Attributen: walk() liest die BasicFileAttributes jedes Eintrags, um Verzeichnisse zu erkennen, und verwirft sie dann – ein Files.isRegularFile() im filter() liest sie erneut vom Dateisystem. find() reicht die bereits gelesenen Attribute an den Matcher weiter – so wie walkFileTree() sie an die Methoden des Visitors weitergibt. Die Javadoc formuliert es vorsichtig: find() kann dadurch effizienter sein als walk() mit filter().
Mit den Attributen kannst du beispielsweise auch nach Größe und Zeitstempeln filtern, ohne das Dateisystem erneut zu befragen. Das folgende Beispiel findet alle Dateien, die größer als 120 Bytes sind:
try (Stream<Path> matches = Files.find(project, Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile() && attrs.size() > 120)) {
matches.forEach(System.out::println);
}
files/project/pom.xml
files/project/README.md
Das Startverzeichnis selbst bekommt der Matcher ebenfalls zu sehen. Ein Matcher, der auf Verzeichnisse prüft, liefert es deshalb mit:
try (Stream<Path> matches =
Files.find(project, 1, (path, attrs) -> attrs.isDirectory())) {
matches.forEach(System.out::println);
}
files/project
files/project/docs
files/project/src
Eine IOException während der Suche verpackt find() wie walk() in eine UncheckedIOException, und ein viertes Argument nimmt FileVisitOption.FOLLOW_LINKS entgegen.
Nach Mustern suchen mit PathMatcher
Die Suche nach .java-Dateien prüft bisher mit endsWith(".java") auf dem String des Pfads. Für komplexere Muster – z. B. mehrere Endungen oder ein bestimmtes Verzeichnis im Pfad – gibt es PathMatcher. Du bekommst ihn von FileSystems.getDefault().getPathMatcher() mit einem Glob- oder einem Regex-Muster, und seine Methode matches() prüft einen Path gegen das Muster.
In einem Glob steht * nur für Zeichen innerhalb eines Pfadbestandteils, ** auch über Verzeichnisgrenzen hinweg. Das Muster **/*.java trifft deshalb jede .java-Datei in beliebiger Tiefe:
PathMatcher javaFiles = FileSystems.getDefault().getPathMatcher("glob:**/*.java");
try (Stream<Path> paths = Files.walk(project)) {
paths.filter(javaFiles::matches).forEach(System.out::println);
}
files/project/src/util/Strings.java
files/project/src/Main.java
Ein Muster ohne **/ trifft den ganzen Pfad nicht – *.java passt nicht auf files/project/src/Main.java, weil * nicht über die Schrägstriche hinwegreicht. Willst du nur den Dateinamen prüfen, wendest du den Matcher auf getFileName() an:
PathMatcher docs = FileSystems.getDefault().getPathMatcher("glob:*.{md,xml}");
try (Stream<Path> paths = Files.walk(project)) {
paths.filter(path -> docs.matches(path.getFileName())).forEach(System.out::println);
}
files/project/pom.xml
files/project/docs/setup.md
files/project/README.md
Mit dem Präfix regex: statt glob: gibst du einen regulären Ausdruck an. Das folgende Muster trifft alles, was unterhalb eines Verzeichnisses src liegt:
PathMatcher inSrc = FileSystems.getDefault().getPathMatcher("regex:.*/src/.*");
try (Stream<Path> paths = Files.walk(project)) {
paths.filter(inSrc::matches).forEach(System.out::println);
}
files/project/src/util
files/project/src/util/Strings.java
files/project/src/Main.java
Der Treffer files/project/src/util ist ein Verzeichnis – ein PathMatcher prüft nur den Pfad, nicht den Typ des Eintrags. Für „nur Dateien“ kombinierst du ihn mit Files::isRegularFile oder mit dem Matcher von find().
Gesperrte Verzeichnisse: AccessDeniedException
Darf dein Programm ein Unterverzeichnis nicht lesen, wirft das Öffnen dieses Verzeichnisses eine AccessDeniedException. Das folgende Beispiel zeigt, wie die vier Wege, einen Verzeichnisbaum rekursiv zu durchsuchen, darauf reagieren: Files.walk(), Files.find(), Files.walkFileTree() und Files.list() mit eigener Rekursion. Das Beispiel legt unter target/restricted/ zwei Verzeichnisse an – public mit der Datei notes.txt und private mit der Datei secret.txt – und entzieht private alle Rechte:
Path restricted = Path.of("target/restricted");
Files.createDirectories(restricted.resolve("public"));
Files.createDirectories(restricted.resolve("private"));
Files.writeString(restricted.resolve("public/notes.txt"), "public notes\n");
Files.writeString(restricted.resolve("private/secret.txt"), "top secret\n");
Files.setPosixFilePermissions(
restricted.resolve("private"), PosixFilePermissions.fromString("---------"));
Im Beispiel-Repository übernimmt das die Methode Examples.createRestrictedTree(), und ein finally-Block gibt die Rechte am Ende zurück, damit sich das Verzeichnis wieder löschen lässt.
Files.walk() gibt die lesbaren Einträge aus und bricht dann ab:
try (Stream<Path> paths = Files.walk(restricted)) {
paths.forEach(System.out::println);
} catch (UncheckedIOException e) {
System.out.println(e);
}
target/restricted
target/restricted/public
target/restricted/public/notes.txt
java.io.UncheckedIOException: java.nio.file.AccessDeniedException: target/restricted/private
Die AccessDeniedException kommt verpackt in einer UncheckedIOException aus forEach(), und damit ist die Suche beendet. Ob nach private noch weitere Verzeichnisse gekommen wären, erfährst du nicht. Files.find() verhält sich genauso:
try (Stream<Path> matches = Files.find(restricted, Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile())) {
matches.forEach(System.out::println);
} catch (UncheckedIOException e) {
System.out.println(e);
}
target/restricted/public/notes.txt
java.io.UncheckedIOException: java.nio.file.AccessDeniedException: target/restricted/private
Weder walk() noch find() bieten eine Möglichkeit, das Verzeichnis zu überspringen und weiterzusuchen.
Files.walkFileTree() ruft für das gesperrte Verzeichnis visitFileFailed() auf. Die Implementierung in SimpleFileVisitor wirft die übergebene Exception weiter – und damit bricht auch walkFileTree() ab, allerdings mit der AccessDeniedException selbst, ohne Verpackung:
try {
Files.walkFileTree(restricted, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
});
} catch (AccessDeniedException e) {
System.out.println(e);
}
target/restricted/public/notes.txt
java.nio.file.AccessDeniedException: target/restricted/private
Hier hast du im Gegensatz zu walk() und find() eine Möglichkeit einzugreifen: Du überschreibst visitFileFailed(), meldest den Fehler und lieferst CONTINUE. walkFileTree() setzt die Suche dann beim nächsten Eintrag fort:
Files.walkFileTree(restricted, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc)
throws IOException {
if (exc instanceof AccessDeniedException) {
System.out.println("Access denied: " + file);
return FileVisitResult.CONTINUE;
}
throw exc;
}
});
target/restricted/public/notes.txt
Access denied: target/restricted/private
Jede andere IOException wirft in dieser Implementierung die Methode weiter, denn ein Lesefehler auf der Festplatte ist etwas anderes als ein fehlendes Leserecht. visitFileFailed() wird übrigens auch aufgerufen, wenn sich die Attribute einer Datei nicht lesen lassen oder wenn FOLLOW_LINKS einen Zyklus findet – dann mit einer FileSystemLoopException.
Bei Files.list() mit eigener Rekursion fängst du die AccessDeniedException am rekursiven Aufruf ab:
private static void listRecursively(Path dir) throws IOException {
try (Stream<Path> entries = Files.list(dir)) {
entries.forEach(entry -> {
System.out.println(entry);
if (Files.isDirectory(entry)) {
try {
listRecursively(entry);
} catch (AccessDeniedException e) {
System.out.println("Access denied: " + entry);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
});
}
}
target/restricted/public
target/restricted/public/notes.txt
target/restricted/private
Access denied: target/restricted/private
Das gesperrte Verzeichnis selbst erscheint in der Ausgabe, denn sein Eintrag im übergeordneten Verzeichnis ist lesbar – nur sein Inhalt nicht. Erst Files.list(entry) für private wirft die Exception, und die fängt der Catch-Block ab.
Die vier Varianten im Vergleich
Die folgende Tabelle stellt die vier Wege, einen Verzeichnisbaum zu durchsuchen, nebeneinander: ob die Methode die Rekursion übernimmt, ob der Filter die Dateiattribute ohne weiteren Zugriff bekommt, ob du eine AccessDeniedException für ein einzelnes Verzeichnis abfangen und danach weitersuchen kannst und ob du einen Stream schließen musst.
| Variante | Rekursion | Attribute im Filter | AccessDeniedExceptioneinzeln abfangbar | Stream schließen nötig |
|---|---|---|---|---|
Files.list() + eigene Rekursion | selbst geschrieben | ✗ | ✓ | ✓ |
Files.walk() | ✓ | ✗ | ✗ | ✓ |
Files.walkFileTree() | ✓ | ✓ | ✓ ( visitFileFailed()) | ✗ (kein Stream) |
Files.find() | ✓ | ✓ | ✗ | ✓ |
Ich empfehle Files.walk(), wenn du die Treffer als Stream weiterverarbeitest und alle Verzeichnisse lesbar sind – die Suche ist dann eine Pipeline aus drei Zeilen. Filterst du nach Größe oder Zeitstempel, solltest du Files.find() nehmen; sein Matcher bekommt die Attribute ohne weiteren Zugriff auf das Dateisystem.
Files.walkFileTree() solltest du in zwei Fällen nehmen: wenn du mit gesperrten Verzeichnissen rechnen musst – etwa beim Durchsuchen eines Home-Verzeichnisses oder eines Netzlaufwerks –, und wenn du vor und nach einem Verzeichnis etwas tun willst, wie beim Kopieren und Löschen ganzer Bäume. Der Preis ist eine anonyme Klasse mit zwei oder drei Methoden statt einer Pipeline.
Die eigene Rekursion mit Files.list() brauchst du nur, wenn du die Reihenfolge selbst bestimmen willst – etwa die Einträge jedes Verzeichnisses sortieren, bevor du sie durchläufst. walk(), find() und walkFileTree() halten sich an die Reihenfolge des Dateisystems.
Die alte API: File.list() und File.listFiles()
Vor Java 7 gab es nur die Klasse java.io.File. Ihre Methode list() liefert die Namen der Einträge als String[], listFiles() die Einträge als File[]:
File project = new File("files/project");
System.out.println(Arrays.toString(project.list()));
for (File file : project.listFiles()) {
String info = file.isDirectory() ? "/" : " (" + file.length() + " bytes)";
System.out.println(file + info);
}
[pom.xml, docs, README.md, src]
files/project/pom.xml (154 bytes)
files/project/docs/
files/project/README.md (145 bytes)
files/project/src/
Beide Methoden sind nicht rekursiv, und beide liefern im Fehlerfall null – wenn der Pfad kein Verzeichnis ist, wenn es ihn nicht gibt oder wenn die Rechte fehlen:
System.out.println(new File("files/project/test").listFiles());
null
Warum die Methode gescheitert ist, erfährst du nicht. Und ein Programm, das nicht auf null prüft, bricht in der For-each-Schleife darüber mit einer NullPointerException ab. Files.list() wirft dagegen eine NotDirectoryException, eine NoSuchFileException oder eine AccessDeniedException – jede mit dem Pfad, um den es geht.
Ein zweiter Unterschied ist der Speicherverbrauch: listFiles() legt ein Array mit allen File-Objekten an, bevor du das erste File bekommst. Ein Verzeichnis mit einer Million Dateien ergibt ein Array mit einer Million File-Objekten. Files.list() hingegen liefert die Einträge nach und nach, während du sie liest.
Für neuen Code empfehle ich Files.list(). Bekommst du von einer älteren API ein File-Objekt, wandelst du es mit toPath() in ein Path-Objekt um. Wie Path und File zusammenhängen, zeigt der Artikel über Datei- und Verzeichnisnamen mit File, Path und Paths.
Häufige Fehler
Die folgenden Fehler zeigen die Abschnitte oben jeweils ausführlich:
- Der Stream wird nicht geschlossen. Jeder Stream aus
list(),walk()undfind()hält ein offenes Verzeichnis; ohne Try-with-resources gehen dem Prozess irgendwann die Datei-Handles aus. Files.list()soll Unterverzeichnisse durchsuchen. Die Methode liefert nur die direkten Einträge; für den ganzen Baum nimmwalk(),walkFileTree()oderfind().- Die Reihenfolge wird als alphabetisch angenommen. Sie ist die des Dateisystems; sortiere explizit mit
sorted(). walk()liefert das Startverzeichnis mit. Das erste Element des Streams ist das Startverzeichnis selbst – einfilter(Files::isRegularFile)nimmt es heraus.Files.isRegularFile()im Matcher vonfind(). Der Matcher bekommt die Attribute schon;attrs.isRegularFile()spart den zweiten Zugriff auf das Dateisystem.- Ein gesperrtes Verzeichnis beendet die Suche.
walk()undfind()werfen eineUncheckedIOExceptionund sind damit beendet; weitersuchen nach einem Fehler kannst du nur mitwalkFileTree()undvisitFileFailed()oder mit eigener Rekursion. glob:*.javaauf dem ganzen Pfad.*reicht nicht über Schrägstriche; für beliebige Tiefe heißt das Muster**/*.java, oder du prüfst nurgetFileName().File.listFiles()ohnenull-Prüfung. Ein fehlendes Verzeichnis liefertnull, und die Schleife darüber wirft eineNullPointerException.
Zusammenfassung
Files.list() liefert die Einträge eines einzelnen Verzeichnisses als Stream<Path>, in der Reihenfolge des Dateisystems und ohne Rekursion. Files.walk() und Files.find() durchlaufen den ganzen Baum in Tiefensuche, beginnend mit dem Startverzeichnis; find() reicht die Dateiattribute an seinen Matcher weiter. Files.walkFileTree() ruft für jeden Eintrag eine Methode einer FileVisitor-Implementierung auf – preVisitDirectory() vor und postVisitDirectory() nach jedem Verzeichnis – und lässt dich mit FileVisitResult Teilbäume überspringen oder die Suche beenden.
Alle drei Stream-Methoden halten offene Verzeichnisse und gehören deshalb in ein Try-with-resources. Eine IOException während der Suche verpacken sie in eine UncheckedIOException – und bei einem Verzeichnis ohne Leserecht endet damit die Suche. Nur walkFileTree() mit überschriebenem visitFileFailed() und die eigene Rekursion mit Files.list() suchen danach weiter.
Meine Empfehlungen:
- Nimm
Files.walk()für die Suche in Verzeichnissen, die dein Programm lesen darf, undFiles.find(), wenn du nach Größe oder Zeitstempel filterst. - Nimm
Files.walkFileTree(), wenn Verzeichnisse gesperrt sein können oder du ganze Bäume kopierst oder löschst. - Schließe jeden Stream aus
list(),walk()undfind()mit Try-with-resources. - Verlass dich nicht auf die Reihenfolge der Einträge; sortiere mit
sorted(), wenn die Reihenfolge eine Rolle spielt. - Prüfe Dateinamen mit einem
PathMatcherund**/im Glob, wennendsWith()nicht reicht. - Lass
File.listFiles()in altem Code, und schreib neuen Code mitFiles.list().
Wie du mit walkFileTree() ein Verzeichnis samt Inhalt kopierst und löschst, zeigt der Artikel über das Erstellen, Kopieren, Verschieben und Löschen von Dateien und Verzeichnissen aus derselben Serie. Wie du die gefundenen Dateien liest, zeigt der Artikel über das Lesen von Dateien.
War dieser Artikel hilfreich für dich? Dann freue ich mich, wenn du dir kurz Zeit für eine Bewertung auf meinem ProvenExpert-Profil nimmst.




