W3docs

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:

OptionBedeutung
READZum Lesen öffnen
WRITEZum Schreiben öffnen
CREATEErstellen, falls nicht vorhanden; nichts tun, falls vorhanden
CREATE_NEWErstellen, falls nicht vorhanden; fehlschlagen, falls vorhanden
APPENDSchreibvorgänge gehen ans Ende der Datei
TRUNCATE_EXISTINGInhalt beim Öffnen leeren
DELETE_ON_CLOSELöschen, wenn der Channel geschlossen wird (temporäre Dateien)
SYNC / DSYNCSchreibvorgä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 absent

Erstellen

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);                       // idempotent

Files.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.

java— editable, runs on the server

Was aus der Ausführung mitzunehmen ist:

  • Files.writeString(...) öffnete die Datei, schrieb den Inhalt und schloss sie — ein einziger Aufruf, wo java.io FileOutputStream + 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 explizite StandardOpenOption.APPEND (zusammen mit WRITE übergeben) die Überschreibung.
  • Files.lines(log).filter(...) erledigte dieselbe Streaming-Lese-Aufgabe wie BufferedReader.lines() mit eingewickelter Öffnen-Schließen-Logik. Das try-with-resources um den Stream ist der Schließmechanismus — ohne es leckt das Dateihandle. Jede Methode auf Files, die einen Stream zurückgibt, ist schließbar; behandle es so.
  • Der Kopierschritt verwendete sowohl REPLACE_EXISTING (Überschreiben erlauben) als auch COPY_ATTRIBUTES (mtime/Besitzer mitführen). Ohne COPY_ATTRIBUTES hätte das Backup eine neue mtime, was für "Ist dieses Backup noch aktuell?"-Prüfungen relevant ist. Files.copy ist standardmäßig konservativ; alles andere muss explizit aktiviert werden.
  • Der atomare-Verschiebungs-Block ist das sichere Schreibmuster: Inhalt nach target.tmp schreiben, dann ATOMIC_MOVE auf 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 auf rename(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 einen Stream<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 unter walkFileTree; 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.

Übung

Übung
Du möchtest die Datei `/var/data/config.json` mit einer neuen Nutzlast überschreiben, aber Leser dürfen niemals einen halb geschriebenen Zustand sehen, wenn die JVM mitten in das Schreiben abstürzt. Welche Abfolge von `Files`-Aufrufen implementiert das sichere Schreibmuster?
Du möchtest die Datei `/var/data/config.json` mit einer neuen Nutzlast überschreiben, aber Leser dürfen niemals einen halb geschriebenen Zustand sehen, wenn die JVM mitten in das Schreiben abstürzt. Welche Abfolge von `Files`-Aufrufen implementiert das sichere Schreibmuster?
Was this page helpful?