W3docs

Dateien schreiben in Java

Textdateien und Binärdateien in Java schreiben mit FileWriter, BufferedWriter, PrintWriter und Files.writeString – mit Beispielen.

Eine Datei in Java zu schreiben bedeutet, Daten im Speicher — einen String, eine List von Zeilen oder ein byte[] — als Bytes auf der Festplatte zu speichern. Dieses Kapitel behandelt die fünf Writer, die man tatsächlich verwendet, wann jeder einzelne passt, die StandardOpenOption-Flags, die das Verhalten bei Überschreiben oder Anhängen bestimmen, und den häufigsten Schreibfehler: Daten, die "nicht gespeichert wurden", weil der Writer nie geschlossen wurde.

Das Schreiben folgt demselben Muster wie das Lesen im vorherigen Kapitel — moderne Einzeiler über Files, klassische Dekoratoren über FileWriter und ein kleiner Satz von Optionen, der bestimmt, was passiert, wenn die Zieldatei vorhanden ist oder nicht.

Files.writeString(path, text) — ganze Datei, ein Aufruf

Das Gegenstück zu Files.readString. Hinzugefügt in Java 11.

Files.writeString(Path.of("notes.txt"), "hello world\n", StandardCharsets.UTF_8);

Die Standard-Öffnungsoptionen sind CREATE, WRITE, TRUNCATE_EXISTING — also "erstellen falls nicht vorhanden, überschreiben falls vorhanden." Diese Standardeinstellung überrascht Entwickler, die Anhänge-Verhalten erwarten; man muss es explizit einschalten:

Files.writeString(path, "another line\n", StandardCharsets.UTF_8,
    StandardOpenOption.CREATE, StandardOpenOption.APPEND);

Gibt den übergebenen Path zurück (praktisch für Verkettungen). Verwenden wenn: man einen kleinen Text hat und einen einzigen Aufruf möchte. Gleicher Speichervorbehalt wie bei readString — keinen 4-GB-String im Speicher aufbauen, nur um ihn zu schreiben.

Files.write(path, lines) und Files.write(path, bytes)

Zwei Überladungen desselben Files.write:

Files.write(Path.of("hosts.txt"), List.of("alpha", "beta", "gamma"), StandardCharsets.UTF_8);
Files.write(Path.of("photo.png"), pngBytes);

Die Iterable<? extends CharSequence>-Überladung schreibt jedes Element in einer eigenen Zeile mit \n-Trennzeichen. Die byte[]-Überladung schreibt rohe Bytes — ideal für Binärdaten, wenn die Bytes bereits im Speicher sind.

Files.newBufferedWriter(path) — die moderne Writer-Factory

Das handle-basierte, streaming-fähige Gegenstück zu Files.newBufferedReader.

try (BufferedWriter w = Files.newBufferedWriter(
        Path.of("out.txt"), StandardCharsets.UTF_8, StandardOpenOption.CREATE)) {
  w.write("first line");
  w.newLine();
  w.write("second line");
  w.newLine();
}

Verwenden wenn: man viele kleine Abschnitte schreibt (eine Schleife über Datensätze, eine Streaming-Transformation, ein Log-Writer) und den gesamten Inhalt nicht zuerst als String materialisieren möchte. Der Puffer bündelt Schreibvorgänge, sodass das Betriebssystem eine Handvoll großer Systemaufrufe statt vieler kleiner sieht.

FileWriter und BufferedWriter — der klassische Stack

Die altbewährte "selbst zusammengebaut"-Variante:

try (BufferedWriter w = new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8))) {
  for (String line : lines) {
    w.write(line);
    w.newLine();
  }
}

Drei Schichten, von unten nach oben: FileWriter schreibt rohe Zeichen mit dem angegebenen Zeichensatz (oder dem Plattformstandard — das sollte man nie tun); BufferedWriter umhüllt ihn mit einem In-Memory-Puffer und einer portablen newLine()-Methode. Gleiche Form, mehr Tipparbeit als die Files.newBufferedWriter-Form. Neuer Code bevorzugt die moderne Factory; diesen Stack findet man in älterem Code.

Das zweite Konstruktorargument von FileWriter ist append:

new FileWriter("out.txt", true);      // append mode (boolean)
new FileWriter("out.txt", StandardCharsets.UTF_8);                 // overwrite, UTF-8
new FileWriter("out.txt", StandardCharsets.UTF_8, true);            // append, UTF-8

Der (String, boolean)-Konstruktor stammt aus der Zeit vor den zeichensatzfähigen Versionen. Beide in derselben Codebasis zu mischen ist eines jener Legacy-Wartungsprobleme — gleiche Klasse, zwei konkurrierende Argumentreihenfolgen.

PrintWriter — formatierte Ausgabe

PrintWriter fügt print, println und printf zu jedem Writer hinzu. Es ist dieselbe API, die man auf System.out verwendet (was selbst ein PrintStream ist, das byte-orientierte Geschwister).

try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(Path.of("report.txt")))) {
  w.println("Report generated");
  w.printf("user = %-10s  total = %d%n", "alice", 42);
  w.printf("user = %-10s  total = %d%n", "bob",   17);
}

Zwei Dinge zu wissen:

  • printf verwendet %n als plattformspezifisches Zeilentrennzeichen. \n ist fest codiertes LF, was man in der Regel für Log-Dateien und maschinenlesbare Daten verwenden möchte.
  • PrintWriter schluckt IOException. print, println und printf werfen keine Ausnahme — sie setzen ein internes Fehler-Flag, das man mit checkError() prüfen kann. Das ist eine bewusste Entscheidung für System.out (Konsolenausgaben sollten kein CLI-Tool zum Absturz bringen), aber es ist eine Fehlerquelle bei Datei-Writern. Wenn zuverlässige Fehlerbehandlung wichtig ist, sollte man false an den entsprechenden Konstruktor übergeben und BufferedWriter für das eigentliche Schreiben verwenden, PrintWriter nur für Formatierungshilfen — oder checkError() nach den Schreibvorgängen abfragen.

StandardOpenOption-Flags

Jeder moderne Writer akzeptiert OpenOption...-Varargs, die die Öffnungssemantik ändern:

OptionBedeutung
CREATEDatei erstellen, falls sie nicht existiert; andernfalls die vorhandene öffnen.
CREATE_NEWErstellen; FileAlreadyExistsException werfen, wenn die Datei existiert. Atomar.
TRUNCATE_EXISTINGFalls die Datei existierte, beim Öffnen leeren.
APPENDAm Ende der Datei schreiben, ohne zu kürzen. Atomar auf den meisten Betriebssystemen.
WRITEZum Schreiben öffnen. Immer für Writer impliziert.
SYNC / DSYNCJeden Schreibvorgang blockieren, bis das Betriebssystem meldet, dass er auf der Festplatte ist. Langsam; Dauerhaftigkeit für Absturzsicherheit.
DELETE_ON_CLOSEDatei löschen, wenn der Stream geschlossen wird.

Die wichtigen Kombinationen:

  • Überschreiben (Standard): CREATE, TRUNCATE_EXISTING. Was Files.writeString und Files.newBufferedWriter standardmäßig verwenden.
  • Anhängen: CREATE, APPEND. Das Log-Datei-Muster.
  • Erstellen oder fehlschlagen: CREATE_NEW. Das Sperrdatei- oder "Nicht-Überschreiben"-Muster.

APPEND ist OS-atomar unter Unix: Zwei Prozesse, die an dieselbe Datei anhängen, erhalten verschachtelte Blöcke, aber keine zerrissenen Schreibvorgänge innerhalb eines einzelnen gepufferten Abschnitts. Das ist der Vertrag, der es zum Standard-Logging-Muster macht.

Die "Writer hat nichts geschrieben"-Falle

Das ist der Fehler, auf den jede Java-Codebasis einmal stößt:

// WRONG — the writer is never closed
BufferedWriter w = Files.newBufferedWriter(path);
w.write("important data");
return;       // tail buffer is still in memory; nothing reached the disk

BufferedWriter (und PrintWriter) bündeln Schreibvorgänge in einem In-Memory-Abschnitt. Bytes gelangen erst dann auf die Festplatte, wenn entweder der Puffer voll ist oder close() ausgeführt wird. Ohne try-with-resources wird der Schließvorgang übersprungen, und die "gespeicherten" Daten verschwinden.

// CORRECT
try (BufferedWriter w = Files.newBufferedWriter(path)) {
  w.write("important data");
}                          // close() runs here; tail buffer is flushed

Wenn Daten vor dem Schließen auf der Festplatte sein müssen — beispielsweise wenn ein Tail-Watcher jede Log-Zeile sehen muss — sollte man flush() explizit aufrufen. Files.newBufferedWriter spült nicht automatisch nach jedem Schreibvorgang; das ist der Preis des Puffers.

Welchen Writer verwenden

SzenarioWahl
Kleiner String, einmaliger AufrufFiles.writeString
Liste von Zeilen oder Byte-ArrayFiles.write
Viele Zeilen streamenFiles.newBufferedWriter
printf-Formatierung benötigtPrintWriter um einen gepufferten Writer
Nur für Legacy-CodeBufferedWriter(new FileWriter(...))

Standardmäßig Files.writeString für "Text liegt bereits vor" und Files.newBufferedWriter für "Zeile für Zeile aufbauen". PrintWriter nur verwenden, wenn man printf benötigt.

Ein ausgearbeitetes Beispiel: alle Writer nebeneinander

Das folgende Programm schreibt denselben Inhalt auf drei verschiedene Arten — moderner Einzel-Aufruf, zeilenweise gestreamt über BufferedWriter und printf-formatiert über PrintWriter — demonstriert dann APPEND versus den Standard TRUNCATE_EXISTING und schließlich den "vergessen zu schließen"-Fehlermodus. Alle Schreibvorgänge richten sich an eine temporäre Datei, damit das Beispiel überall läuft.

java— editable, runs on the server

Was man aus dem Ergebnis mitnehmen kann:

  • Files.writeString und Files.write(List) sind die richtigen Aufrufe, wenn der gesamte Inhalt bereits vorliegt. Beide haben die Datei jedes Mal überschrieben, da ihre Standardoptionen TRUNCATE_EXISTING enthalten.
  • BufferedWriter und PrintWriter liefen innerhalb von try-with-resources. Das ist das Einzige, das garantiert, dass der Endpuffer auf die Festplatte gelangt — lässt man es weg, hat man einen "Writer hat nichts geschrieben"-Fehler im Code.
  • Die APPEND/TRUNCATE-Sequenz schrieb base, hängte appended an, kürzte dann und schrieb truncated. Die endgültige Datei enthielt nur truncated\n — das ist die Falle: Der Standardmodus jedes modernen Writers ist Überschreiben, nicht Anhängen. Man muss sich explizit dafür entscheiden.
  • CREATE_NEW auf einem vorhandenen Pfad warf FileAlreadyExistsException. Das ist die "Nicht-Überschreiben"-Semantik — nützlich für Sperrdateien und atomare "Bin ich schon gelaufen?"-Marker.
  • Der undichte Writer hatte vor dem Aufruf von flush() eine Dateigröße von 0. Die Bytes waren im Speicher, nicht auf der Festplatte; ohne das manuelle flush() (oder ein ordnungsgemäßes close()) wären sie verloren gegangen.

Was kommt als Nächstes

Das nächste Kapitel, Dateien löschen in Java, schließt die "High-Level-Dateioperationen"-Kapitel mit den drei Löschvorgängen ab: File.delete(), Files.delete() und Files.deleteIfExists() — und wie man einen Verzeichnisbaum entfernt, ohne die Rekursion selbst zu schreiben.

Übungen

Übung
`Files.writeString(path, text)` ohne `OpenOption`-Argumente. Was passiert, wenn die Datei bereits existiert?
`Files.writeString(path, text)` ohne `OpenOption`-Argumente. Was passiert, wenn die Datei bereits existiert?
Was this page helpful?