Java NIO Files-Klasse
Dateisystemoperationen in Java mit java.nio.file.Files — lesen, schreiben, kopieren, verschieben, durchlaufen.
Path (das vorherige Kapitel) war das Substantiv. Files ist das Verb — eine statische Hilfsklasse, deren jede Methode einen Path entgegennimmt und irgendetwas mit der Datei an diesem Pfad tut. Hier befinden sich die Einzeiler, die den Rest dieses Teils still und leise kürzer gemacht haben: Files.readString, Files.newBufferedReader, Files.createTempFile, Files.size. Dieses Kapitel geht den vollständigen Katalog durch.
Files ist umfangreich — etwa 80 Methoden — und nach Zweck gruppiert: lesen, schreiben, erstellen, prüfen, ändern, durchlaufen. Man muss es sich nicht merken; man muss wissen, dass es der erste Anlaufpunkt ist, wenn man irgendetwas mit einer Datei tun möchte.
Lesen
Die Ganz-Datei-Leser sind jeweils eine Zeile:
String text = Files.readString(path); // UTF-8 by default (Java 11+)
String utf16 = Files.readString(path, StandardCharsets.UTF_16);
byte[] bytes = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);Für Dateien, die klein genug sind, um in den Speicher zu passen, sind readString und readAllBytes die richtigen Werkzeuge. Sie öffnen die Datei, lesen alles, schließen sie und geben den Inhalt zurück. Keine Streams, keine Puffer, keine Schließlogik.
Für Dateien, die zu groß sind, um vollständig geladen zu werden, verwendet man die Streaming-Formen:
try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = r.readLine()) != null) process(line);
}
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.filter(...).forEach(...); // closes the file when the stream closes
}
try (InputStream in = Files.newInputStream(path)) {
// raw bytes for binary formats
}Files.lines ist BufferedReader.lines mit der Öffnen-Schließen-Logik eingewickelt. Das try-with-resources um den Stream übernimmt das Schließen — ohne es leckt das Dateihandle.
Schreiben
Auf der Schreibseite dasselbe Muster:
Files.writeString(path, "hello\n", StandardCharsets.UTF_8);
Files.write(path, bytes); // byte[]
Files.write(path, lines, StandardCharsets.UTF_8); // Iterable<? extends CharSequence>Alle drei sind atomare Einzelaufrufe: öffnen, schreiben, schließen. Standardmäßig erstellen oder kürzen sie — wenn die Datei existierte, ist ihr vorheriger Inhalt weg. Zum Anhängen:
Files.writeString(path, "more\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);Für die Streaming-Form (inkrementelles Schreiben):
try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
for (String line : lines) w.write(line);
}Öffnungsoptionen
Jede Lese-/Schreibmethode, die eine Datei öffnet, akzeptiert ein optionales Varargs von StandardOpenOption:
| Option | Bedeutung |
|---|---|
READ | Zum Lesen öffnen |
WRITE | Zum Schreiben öffnen |
CREATE | Erstellen, falls nicht vorhanden; nichts tun, falls vorhanden |
CREATE_NEW | Erstellen, falls nicht vorhanden; fehlschlagen, falls vorhanden |
APPEND | Schreibvorgänge gehen ans Ende der Datei |
TRUNCATE_EXISTING | Inhalt beim Öffnen leeren |
DELETE_ON_CLOSE | Löschen, wenn der Channel geschlossen wird (temporäre Dateien) |
SYNC / DSYNC | Schreibvorgänge blockieren, bis das OS bestätigt, dass die Daten auf dem Datenträger sind |
Der Standard-Öffnungsmodus für newBufferedWriter und writeString ist CREATE, TRUNCATE_EXISTING, WRITE. Der Standard für newBufferedReader und readString ist READ. Explizite Optionen überschreiben die Standardwerte — die Angabe irgendeiner Option schaltet die implizite Menge aus, daher müssen die impliziten normalerweise wiederholt werden, wenn man anpasst:
Files.newBufferedWriter(path, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND); // appends, creates if absentErstellen
Files.createFile(path); // empty file; fails if it exists
Files.createDirectory(path); // single dir; fails if parent absent
Files.createDirectories(path); // recursive: like `mkdir -p`
Files.createSymbolicLink(link, target);
Files.createLink(link, target); // hard link
Path tmpFile = Files.createTempFile("prefix-", ".txt"); // in the default temp dir
Path tmpDir = Files.createTempDirectory("prefix-");createDirectories ist das richtige Werkzeug für "Ich möchte, dass dieses Verzeichnis existiert." Es ist idempotent: Wenn das Verzeichnis bereits vorhanden ist, kehrt es ohne Fehler zurück; wenn ein Vorfahre fehlt, erstellt es die gesamte Kette. createDirectory (ohne -ies) erstellt nur eine Ebene und schlägt fehl, wenn das übergeordnete Verzeichnis nicht existiert — fast immer falsch, es sei denn, man benötigt diese Prüfung ausdrücklich.
Für temporäre Dateien wählen die Überladungen createTempFile und createTempDirectory automatisch das System-Temp-Verzeichnis und geben den erstellten Path zurück. Paare sie mit .toFile().deleteOnExit() für die Bereinigung, oder führe explizites Files.delete in einem finally durch.
Prüfen
Die Prädikate und Zugriffsmethoden:
boolean ok = Files.exists(path);
boolean nope = Files.notExists(path); // NOT the negation of exists
boolean file = Files.isRegularFile(path);
boolean dir = Files.isDirectory(path);
boolean link = Files.isSymbolicLink(path);
boolean read = Files.isReadable(path);
boolean write = Files.isWritable(path);
boolean exec = Files.isExecutable(path);
long size = Files.size(path); // throws IOException
FileTime mtime = Files.getLastModifiedTime(path);
String mimeType = Files.probeContentType(path); // best-effort, can return null
UserPrincipal owner = Files.getOwner(path);exists und notExists sind keine Negationen voneinander: Beide können false zurückgeben, wenn der Zugriff auf die Datei nicht bestimmt werden kann (Zugriff verweigert, hängender Symlink). Verwende das richtige für das, was man braucht — !exists(p) und notExists(p) unterscheiden sich in Randfällen.
Kopieren, verschieben, löschen
Files.copy(source, target); // fails if target exists
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.copy(source, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES); // copy mtime/owner too
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); // rename within a filesystem; rename-or-fail
Files.delete(path); // throws if absent
boolean deleted = Files.deleteIfExists(path); // idempotentFiles.move mit ATOMIC_MOVE ist das richtige Werkzeug für "In eine temporäre Datei schreiben, dann die Live-Datei atomar ersetzen." Auf demselben Dateisystem wird es auf rename(2) abgebildet; die Live-Datei wechselt in einem einzigen Moment von alt zu neu, ohne Zwischenzustand. So baut man absturzsichere Schreibvorgänge:
Path tmp = path.resolveSibling(path.getFileName() + ".tmp");
Files.writeString(tmp, content, StandardCharsets.UTF_8);
Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);Wenn die JVM nach writeString, aber vor move abstürzt, bleibt die Live-Datei unberührt.
Auflisten und durchlaufen
try (Stream<Path> entries = Files.list(directory)) {
entries.forEach(System.out::println); // direct children only
}
try (Stream<Path> tree = Files.walk(directory)) {
tree.filter(Files::isRegularFile).forEach(...); // recursive
}
try (Stream<Path> tree = Files.walk(directory, 2)) { // depth-limited
...
}
try (Stream<Path> found = Files.find(directory, Integer.MAX_VALUE,
(p, attrs) -> attrs.isRegularFile() && p.toString().endsWith(".log"))) {
...
}Diese sollten immer mit try-with-resources verwendet werden — der zugrundeliegende DirectoryStream ist offen, bis der Stream geschlossen wird. Ohne das Schließen hält die JVM ein Verzeichnis-Handle bis zur Garbage Collection, was bei einem langlebigen Prozess "nie" bedeutet. Das nächste Kapitel, Java Walk File Tree, geht tiefer auf den Walker ein.
Warum dieses Kapitel kurz ist
Files benötigt nicht viel Erklärung. Jede Methode tut eine Sache, die Namen sind beschreibend, die Parameter sind Path, Charset und Option. Die kognitive Last liegt im Katalog — zu wissen, was verfügbar ist — nicht im Verhalten einer einzelnen Methode. Lies das Javadoc für java.nio.file.Files einmal durch; komm zurück, wenn du ein Verb brauchst, das du nicht mehr weißt.
Ein ausgearbeitetes Beispiel: der vollständige Lebenszyklus
Das folgende Programm erstellt ein temporäres Verzeichnis, schreibt eine kleine Textdatei mit writeString, liest sie mit readString zurück, hängt mit der richtigen Öffnungsoption an, kopiert die Datei, verschiebt sie atomar, listet das Verzeichnis bei jedem Schritt auf und räumt schließlich mit deleteIfExists auf. Es ist der alltägliche Java-Datei-Lebenszyklus in einer einzigen main-Methode komprimiert.
Was aus der Ausführung mitzunehmen ist:
Files.writeString(...)öffnete die Datei, schrieb den Inhalt und schloss sie — ein einziger Aufruf, wojava.ioFileOutputStream+OutputStreamWriter(UTF-8)+BufferedWriter+try-with-resources gewollt hätte. Der Standard "beim Öffnen kürzen" ist genau das, was "diesen Inhalt speichern" erwartet. Wenn der vorhandene Inhalt erhalten bleiben soll, ist das expliziteStandardOpenOption.APPEND(zusammen mitWRITEübergeben) die Überschreibung.Files.lines(log).filter(...)erledigte dieselbe Streaming-Lese-Aufgabe wieBufferedReader.lines()mit eingewickelter Öffnen-Schließen-Logik. Dastry-with-resources um denStreamist der Schließmechanismus — ohne es leckt das Dateihandle. Jede Methode aufFiles, die einenStreamzurückgibt, ist schließbar; behandle es so.- Der Kopierschritt verwendete sowohl
REPLACE_EXISTING(Überschreiben erlauben) als auchCOPY_ATTRIBUTES(mtime/Besitzer mitführen). OhneCOPY_ATTRIBUTEShätte das Backup eine neue mtime, was für "Ist dieses Backup noch aktuell?"-Prüfungen relevant ist.Files.copyist standardmäßig konservativ; alles andere muss explizit aktiviert werden. - Der atomare-Verschiebungs-Block ist das sichere Schreibmuster: Inhalt nach
target.tmpschreiben, dannATOMIC_MOVEauf den Live-Namen anwenden. Wenn die JVM mitten im Schreiben abstürzt, ist die Live-Datei unverändert; wenn das Umbenennen erfolgreich ist, wechselt die Live-Datei in einem Moment. Auf demselben Dateisystem wird dies aufrename(2)abgebildet — es gibt keinen Kopierschritt. Verwende dies für jede Datei, bei der Leser niemals einen halb geschriebenen Zustand sehen sollten (Konfiguration, Speicherdateien, generierte Assets). Files.walk(dir)erzeugte einenStream<Path>aller Einträge unter dem Verzeichnis in Tiefenreihenfolge. Die Bereinigung in Schritt 10 sortierte umgekehrt, sodass Kinder vor Eltern gelöscht wurden — derselbe Trick, den man bei einem echten rekursiven Löschen verwenden würde. (Der vollständige Delete-Tree-Helfer befindet sich im nächsten Kapitel unterwalkFileTree; die Streaming-Form hier ist die kürzere Version für kleine Bäume.)
Was kommt als Nächstes
Files deckte die Operationen ab, die auf eine einzelne Datei oder eine einzelne Verzeichnisebene wirken. Das nächste Kapitel, Java Walk File Tree, geht tiefer auf das Durchlaufen eines ganzen Verzeichnisbaums ein — Files.walkFileTree, FileVisitor, Überspringen von Teilbäumen, die Visitor-Pattern-API, die die Fälle behandelt, die die Stream-Form nicht kann.