1. NIO2: genauer kennenlernen
Wir haben NIO2 bereits kennengelernt, aber jetzt wiederholen wir und vertiefen unser Wissen über diese nützliche Bibliothek für die Arbeit mit Dateien und Verzeichnissen.
Früher gab es in Java nur die Klasse File. Sie konnte die Existenz einer Datei prüfen, Dateien und Ordner erstellen und löschen sowie die Dateiliste in einem Verzeichnis abrufen. Aber sie hatte viele Einschränkungen:
- Arbeiten mit Pfaden ist umständlich, insbesondere wenn unterschiedliche Betriebssysteme berücksichtigt werden müssen (C:\Users\user\file.txt unter Windows und /home/user/file.txt unter Linux);
- es fehlt eine vernünftige Unterstützung für symbolische Links, Berechtigungen und Dateiattribute;
- die Möglichkeiten zum Durchlaufen des Verzeichnisbaums sind begrenzt;
- die Fehlerbehandlung ließ zu wünschen übrig.
Mit dem Erscheinen von NIO2 (New Input/Output, Version 2) in Java 7 wurde das Leben der Entwickler einfacher und angenehmer. Jetzt stehen zur Verfügung:
- die Klasse Path für die komfortable Arbeit mit Pfaden zu Dateien und Ordnern;
- die Klasse Files, die alle grundlegenden Operationen bietet: Lesen, Schreiben, Kopieren, Löschen, Abrufen von Informationen über Dateien;
- das Interface FileVisitor und Methoden wie Files.walk, die ein einfaches und flexibles Durchlaufen des Dateisystems ermöglichen.
Warum ist das wichtig?
- Plattformunabhängigkeit: Der gleiche Code läuft unter Windows, Linux und macOS, ohne über Trennzeichen (/ oder \) nachdenken zu müssen.
- Sicherheit und Komfort: Mehr Informationen bei Fehlern, weniger Magie und weniger böse Überraschungen.
- Leistungsfähigkeit: Sehr große Verzeichnisse lassen sich verarbeiten und sogar rekursiv mit Filterung und paralleler Verarbeitung durchlaufen.
2. Zentrale Klassen: Path und Files
Klasse Path
Path — ist die moderne Repräsentation eines Pfads zu einer Datei oder einem Ordner. Er zeigt nicht zwingend auf eine tatsächlich existierende Datei — es ist einfach ein Pfad, mit dem sich bequem arbeiten lässt.
Einen Path erhalten
import java.nio.file.Path;
import java.nio.file.Paths;
Path path1 = Paths.get("file.txt"); // relativer Pfad
Path path2 = Paths.get("/home/user/file.txt"); // absoluter Pfad
Path path3 = Path.of("mydir", "subdir", "file.txt"); // ab Java 11+
Fakt: Path ist betriebssystemunabhängig. Vergessen Sie das manuelle Aneinanderfügen von Strings mit / oder \!
In String umwandeln
System.out.println(path1.toString());
Elternverzeichnis und Dateinamen ermitteln
Path parent = path1.getParent(); // kann bei relativen Pfaden null sein
Path fileName = path1.getFileName(); // nur der Dateiname
Klasse Files
Files ist eine Sammlung statischer Methoden für alle Datei- und Verzeichnisoperationen:
- Existenzprüfung: Files.exists(path)
- Dateien lesen und schreiben: Files.readAllBytes(path), Files.write(path, bytes)
- Informationen abrufen: Files.size(path), Files.getLastModifiedTime(path)
- Kopieren, Löschen, Verschieben: Files.copy, Files.delete, Files.move
Beispiele:
import java.nio.file.Files;
import java.nio.file.Path;
Path path = Path.of("file.txt");
if (Files.exists(path)) {
System.out.println("Datei existiert!");
System.out.println("Größe: " + Files.size(path) + " Byte");
System.out.println("Letzte Änderung: " + Files.getLastModifiedTime(path));
} else {
System.out.println("Datei nicht gefunden.");
}
3. Das Dateisystem durchlaufen: Files.walk und Co.
Problem des alten Ansatzes
Im alten API musste man, um alle Dateien in einem Ordner und dessen Unterordnern zu durchlaufen, rekursive Funktionen schreiben, manuell prüfen, was Datei und was Ordner ist, und darauf achten, nicht in eine Endlosrekursion zu geraten. Das war nicht nur mühsam, sondern auch sehr fehleranfällig.
Moderner Ansatz: Files.walk
Files.walk(Path start) gibt einen Stream<Path> zurück — einen Stream aller Dateien und Ordner ab dem angegebenen Pfad, einschließlich aller Unterverzeichnisse. Das Durchlaufen des Dateisystems ist jetzt einfach Arbeit mit Streams!
Beispiel: Alle Dateien und Ordner ausgeben
import java.nio.file.*;
try (var paths = Files.walk(Path.of("mydir"))) {
paths.forEach(System.out::println);
}
Hier werden alle Pfade ausgegeben: sowohl Dateien als auch Ordner, beginnend bei mydir.
Beispiel: Nur Dateien (ohne Ordner)
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
Die Methode Files.isRegularFile(path) gibt true nur für reguläre Dateien zurück (keine Ordner, keine Symlinks).
Beispiel: Dateien nach Erweiterung suchen
Angenommen, wir müssen alle .txt-Dateien im Verzeichnis und in Unterverzeichnissen finden:
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
Beispiel: Gesamtgröße aller Dateien berechnen
long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
totalSize = paths.filter(Files::isRegularFile)
.mapToLong(path -> {
try {
return Files.size(path);
} catch (Exception e) {
System.err.println("Fehler beim Lesen der Größe: " + path);
return 0L;
}
})
.sum();
}
System.out.println("Gesamtgröße der Dateien: " + totalSize + " Byte");
Wichtig!
- Die Methode Files.walk gibt einen Stream zurück, der geschlossen werden muss (sie implementiert AutoCloseable). Deshalb verwenden wir try-with-resources!
- Standardmäßig wird bis ganz nach unten in die Tiefe gegangen (alle Unterverzeichnisse). Die Tiefe kann begrenzt werden: Files.walk(path, maxDepth)
4. Praxisaufgaben
Aufgabe 1: Alle Bilder im Verzeichnis finden
Es sollen alle Dateien mit den Endungen .jpg, .png, .gif im Ordner images gefunden und ihre Namen ausgegeben werden.
import java.nio.file.*;
import java.util.Set;
Set<String> extensions = Set.of(".jpg", ".png", ".gif");
try (var paths = Files.walk(Path.of("images"))) {
paths.filter(Files::isRegularFile)
.filter(path -> {
String name = path.getFileName().toString().toLowerCase();
return extensions.stream().anyMatch(name::endsWith);
})
.forEach(System.out::println);
}
Aufgabe 2: Alle .txt-Dateien in einen anderen Ordner kopieren
import java.nio.file.*;
Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");
try (var paths = Files.walk(sourceDir)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
try {
Path relative = sourceDir.relativize(path);
Path target = destDir.resolve(relative);
Files.createDirectories(target.getParent());
Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Kopiert: " + path + " -> " + target);
} catch (Exception e) {
System.err.println("Kopierfehler: " + path);
}
});
}
Hier behalten wir die Struktur der Unterverzeichnisse bei.
5. Nützliche Details
Vorteile von NIO2
Plattformunabhängigkeit
Path kümmert sich selbst um die Ordnertrennzeichen. Ihr Code wird unter Windows, Linux und macOS gleichermaßen funktionieren.
Stream-Verarbeitung
Methoden wie Files.walk geben einen Stream (Stream<Path>) zurück, den Sie filtern, transformieren, in Kollektionen sammeln können — alles, was das Stream-API kann.
Arbeiten mit großen Verzeichnissen
Das alte API konnte „abstürzen“, wenn es zu viele Dateien gab (z. B. 100 000 Fotos). NIO2 bewältigt solche Fälle problemlos, da nicht alles auf einmal in den Speicher geladen wird.
Unterstützung von Symlinks, Attributen und Berechtigungen
Man kann feststellen, ob ein Pfad ein symbolischer Link ist (Files.isSymbolicLink(path)), Berechtigungen abrufen (Files.getPosixFilePermissions(path)), den Besitzer der Datei ermitteln und vieles mehr.
Vergleich von altem und neuem API
| Operation | Altes API (File) | Neues API (Path, Files) |
|---|---|---|
| Existenz prüfen | |
|
| Größe ermitteln | |
|
| Dateiliste im Ordner | |
|
| Rekursives Durchlaufen | Rekursion von Hand | |
| Datei kopieren | file.renameTo() (problematisch) | |
| Erweiterung ermitteln | String parsen | |
| Elternverzeichnis ermitteln | |
|
| Mit Berechtigungen arbeiten | So gut wie gar nicht | |
Wichtige Besonderheiten
Dateityp prüfen
- Files.isRegularFile(path) — reguläre Datei
- Files.isDirectory(path) — Verzeichnis
- Files.isSymbolicLink(path) — Symlink
Arbeiten mit großen Verzeichnissen
- Sammeln Sie nicht alle Pfade in einer Liste: Arbeiten Sie mit Streams (Stream<Path>) und verarbeiten Sie sie fortlaufend.
- Nach Abschluss wird der Stream unbedingt geschlossen (try-with-resources).
Ausnahmen
- Fast alle Methoden können eine IOException werfen — denken Sie daran, Fehler zu behandeln (oder weiterzureichen).
Durchlauftiefe begrenzen
try (var paths = Files.walk(Path.of("mydir"), 2)) { // nur 2 Ebenen
// ...
}
6. Typische Fehler beim Arbeiten mit NIO2
Fehler Nr. 1: Stream von walk vergessen zu schließen. Wenn Sie try-with-resources nicht verwenden, kann es zu Ressourcenlecks kommen — der Dateisystem-Stream bleibt geöffnet. Verwenden Sie stets die Konstruktion try (var paths = Files.walk(...)) { ... }.
Fehler Nr. 2: Nicht geprüft, ob der Pfad ein Verzeichnis ist. Wenn Sie an Files.walk einen Pfad zu einer Datei statt zu einem Ordner übergeben, kann es zu unerwartetem Verhalten oder einem Fehler kommen.
Fehler Nr. 3: Ausnahmen nicht behandelt. Praktisch alle Methoden von NIO2 können eine IOException werfen. Ignorieren Sie diese Fehler nicht — geben Sie zumindest eine Meldung für den Nutzer aus oder loggen Sie sie.
Fehler Nr. 4: Verwechslung bei Pfad-Trennzeichen. Wenn Sie Pfade manuell mit / oder \ zusammenbauen, ist das unnötig! Verwenden Sie Path.of(...) oder resolve(...) — diese Methoden regeln das betriebssystemabhängig korrekt.
Fehler Nr. 5: Versuch, ein riesiges Verzeichnis „in den Speicher“ zu laden. Sammeln Sie nicht alle Pfade in einer Liste, wenn es sehr viele Dateien gibt — arbeiten Sie mit Streams (Stream<Path>) und verarbeiten Sie sie fortlaufend.
Fehler Nr. 6: Plattformunabhängigkeit vergessen. Hardcoden Sie keine absoluten Pfade im Windows- oder Unix-Stil. Verwenden Sie Path sowie Operationen wie resolve/relativize — das funktioniert auf jedem OS korrekt.
GO TO FULL VERSION