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-8Der (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:
printfverwendet%nals plattformspezifisches Zeilentrennzeichen.\nist fest codiertes LF, was man in der Regel für Log-Dateien und maschinenlesbare Daten verwenden möchte.PrintWriterschlucktIOException.print,printlnundprintfwerfen keine Ausnahme — sie setzen ein internes Fehler-Flag, das man mitcheckError()prüfen kann. Das ist eine bewusste Entscheidung fürSystem.out(Konsolenausgaben sollten kein CLI-Tool zum Absturz bringen), aber es ist eine Fehlerquelle bei Datei-Writern. Wenn zuverlässige Fehlerbehandlung wichtig ist, sollte manfalsean den entsprechenden Konstruktor übergeben undBufferedWriterfür das eigentliche Schreiben verwenden,PrintWriternur für Formatierungshilfen — odercheckError()nach den Schreibvorgängen abfragen.
StandardOpenOption-Flags
Jeder moderne Writer akzeptiert OpenOption...-Varargs, die die Öffnungssemantik ändern:
| Option | Bedeutung |
|---|---|
CREATE | Datei erstellen, falls sie nicht existiert; andernfalls die vorhandene öffnen. |
CREATE_NEW | Erstellen; FileAlreadyExistsException werfen, wenn die Datei existiert. Atomar. |
TRUNCATE_EXISTING | Falls die Datei existierte, beim Öffnen leeren. |
APPEND | Am Ende der Datei schreiben, ohne zu kürzen. Atomar auf den meisten Betriebssystemen. |
WRITE | Zum Schreiben öffnen. Immer für Writer impliziert. |
SYNC / DSYNC | Jeden Schreibvorgang blockieren, bis das Betriebssystem meldet, dass er auf der Festplatte ist. Langsam; Dauerhaftigkeit für Absturzsicherheit. |
DELETE_ON_CLOSE | Datei löschen, wenn der Stream geschlossen wird. |
Die wichtigen Kombinationen:
- Überschreiben (Standard):
CREATE, TRUNCATE_EXISTING. WasFiles.writeStringundFiles.newBufferedWriterstandardmäß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 diskBufferedWriter (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 flushedWenn 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
| Szenario | Wahl |
|---|---|
| Kleiner String, einmaliger Aufruf | Files.writeString |
| Liste von Zeilen oder Byte-Array | Files.write |
| Viele Zeilen streamen | Files.newBufferedWriter |
printf-Formatierung benötigt | PrintWriter um einen gepufferten Writer |
| Nur für Legacy-Code | BufferedWriter(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.
Was man aus dem Ergebnis mitnehmen kann:
Files.writeStringundFiles.write(List)sind die richtigen Aufrufe, wenn der gesamte Inhalt bereits vorliegt. Beide haben die Datei jedes Mal überschrieben, da ihre StandardoptionenTRUNCATE_EXISTINGenthalten.BufferedWriterundPrintWriterliefen innerhalb vontry-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ängteappendedan, kürzte dann und schriebtruncated. Die endgültige Datei enthielt nurtruncated\n— das ist die Falle: Der Standardmodus jedes modernen Writers ist Überschreiben, nicht Anhängen. Man muss sich explizit dafür entscheiden. CREATE_NEWauf einem vorhandenen Pfad warfFileAlreadyExistsException. 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 manuelleflush()(oder ein ordnungsgemäßesclose()) 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.