

Nachdem es im ersten Teil der Serie um das Lesen von Dateien in Java ging, stelle ich dir in diesem zweiten Teil die korrespondierenden Methoden zum Schreiben von kleinen und großen Dateien vor.
Im einzelnen geht es um folgende Fragen:
- Wie schreibt man am einfachsten einen String (oder eine Liste von Strings) in eine Textdatei?
- Wie schreibt man ein Byte-Array in eine Binärdatei?
- Wie schreibt man beim Verarbeiten von großen Datenmengen diese direkt in Dateien (ohne zunächst den kompletten Inhalt der Datei im Speicher aufbauen zu müssen)?
- Wann verwendet man
FileWriter
,FileOutputStream
,OutputStreamReader
,BufferedOutputStream
undBufferedWriter
? - Wann verwendet man
Files.newOutputStream()
undFiles.newBufferedWriter()
?
Bereits im ersten Teil angesprochen habe ich das Thema Betriebssystem-Unabhängigkeit, d. h. was bei Zeichenkodierungen, Zeilenumbrüche und Pfadnamen zu beachten ist.
Wie schreibt man in Java am einfachsten in eine Datei?
Bis einschließlich Java 6 gab es keine einfache Möglichkeit Dateien zu schreiben. Man musste einen FileOutputStream
oder einen FileWriter
öffnen, diesen ggf. noch mit einem BufferedOutputStream
oder BufferedWriter
wrappen, in diesen schreiben, und anschließend - auch im Fehlerfall - alle Streams wieder schließen.
In Java 7 kam mit der "NIO.2 File API" (NIO steht für New I/O) die Utility-Klasse java.nio.file.Files
hinzu. Diese enthält Methoden, mit denen ein Byte-Array, ein String oder eine Liste von Strings mit einem einzigen Kommando in eine Datei geschrieben werden kann.
Schreiben eines Byte-Arrays in eine Binärdatei
Ein Byte-Array lässt sich mit folgendem Kommando in eine Datei schreiben:
String fileName = ...;
byte[] bytes = ...;
Files.write(Path.of(fileName), bytes);
Code-Sprache: Java (java)
Die Methode erwartet an erster Stelle ein Path
-Objekt. Dieses beschreibt einen Datei- oder Verzeichnisnamen und bietet Hilfsmethoden zur Konstruktion solcher. Im Beispiel wird die statische – seit Java 11 verfügbare – Methode Path.of()
verwendet, um ein Path
-Objekt aus einem Dateinamen zu erstellen. Vor Java 11 kannst du stattdessen Paths.get()
verwenden. Intern rufen beide Methoden FileSystems.getDefault().getPath()
auf.
Schreiben eines Strings in eine Textdatei
Ebenso einfach lässt sich – allerdings erst seit Java 11 – ein String in eine Datei schreiben:
String fileName = ...;
String text = ...;
Files.writeString(Path.of(fileName), text);
Code-Sprache: Java (java)
Schreiben einer Liste von Strings in eine Textdatei
Oft schreibt man nicht einen einzelnen String in eine Textdatei, sondern mehrere in Form von Zeilen. Mit folgendem Kommando schreibst du eine String-Liste (bzw. genauer gesagt ein Iterable<? extends CharSequence>
in eine Textdatei:
String fileName = ...;
List<String> lines = ...;
Files.write(Path.of(fileName), lines);
Code-Sprache: Java (java)
Schreiben von einem String-Stream in eine Textdatei
Es gibt keine 1:1-Entsprechung der Stream-erzeugenden Methode Files.lines()
, also keine Methode, die direkt aus einem String-Stream in eine Datei schreibt. Mit einem kleinen Workaround ist es dennoch möglich:
String fileName = ...;
Stream<String> lines = ...;
Files.write(Path.of(fileName), (Iterable<String>) lines::iterator);
Code-Sprache: Java (java)
Was wird hier gemacht? Die Files.write()
-Methode, die hier verwendet wird, ist dieselbe wie im vorherigen Beispiel, also die, die eine Iterable<String>
entgegennimmt. Ein Java 8-Stream selbst ist kein Iterable
, da man über diesen nicht mehrfach iterieren kann, sondern nur ein einziges Mal. Daher kann man den Stream selbst nicht als Parameter übergeben.
Iterable
ist allerdings ein funktionales Interface, dessen einzige Methode iterator()
einen Iterator
zurückliefert. Daher können wir hier als Iterable
eine Methodenreferenz auf lines.iterator()
übergeben (welche ja auch einen Iterator
zurückliefert). Dies funktioniert, da wir annehmen können, dass Files.write()
die referenzierte iterator()
-Methode nur ein einziges Mal aufruft. Würde die iterator()
-Methode ein zweites mal aufgerufen werden, würde der Stream das mit einer IllegalStateException
mit der Nachricht "stream has already been operated upon or closed" quittieren.
Dateien schreiben mit java.nio.file.Files – Zusammenfassung
Im diesem Kapitel hast du die Utility-Methoden der java.nio.file.Files
-Klasse kennengelernt. Diese sind geeignet für all diejenigen Use Cases, bei denen die zu schreibenden Daten komplett im Arbeitsspeicher liegen.
Werden die Daten jedoch nach und nach generiert, solltest du diese auch nach und nach in eine Datei schreiben. Du solltest sie nicht erst im Arbeitsspeicher "sammeln" und anschließend mit einer der zuvor gezeigten Methoden in einem Rutsch in eine Datei schreiben. Es sei denn, die Menge der Daten ist nur wenige Kilobyte, dann ist das auch in Ordnung.
Besser arbeitest du in einem solchen Fall aber (direkt oder indirekt) mit einem FileOutputStream
. Wie das funktioniert, erfährst du im folgenden Kapitel.
Wie schreibt man anfallende Daten in eine Datei, ohne deren Inhalt erst komplett im Arbeitsspeicher aufbauen zu müssen?
Um Daten nach und nach in eine Datei zu schreiben, verwendet man einen FileOutputStream
(oder damit verwandte Klassen). Diese waren bereits vor Java 7 verfügbar und haben das Schreiben von kleinen Dateien unnötig kompliziert gemacht. Im folgenden stelle ich die verschiedenen Möglichkeiten vor.
Schreiben einzelner Bytes mit dem FileOutputStream
Die zentrale Klasse ist der FileOutputStream
, dieser schreibt Daten Byte für Byte in eine Datei. Das folgende Beispiel zeigt, wie Bytes, die von der Methode process()
zurückgeliefert werden, nach und nach in eine Datei geschrieben werden (solange bis die Methode -1 zurückliefert):
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName)) {
int b;
while ((b = process()) != -1) {
out.write(b);
}
}
Code-Sprache: Java (java)
Das Schreiben einzelner Bytes ist eine teure Funktionalität. Das Schreiben von 100.000.000 Bytes in eine Testdatei dauert auf diese Weise auf meinem System etwa 230 Sekunden, das sind nur wenig mehr als 0,4 MB pro Sekunde.
Schreiben von Byte-Arrays mit dem FileOutputStream
Mit dem FileOutputStream
können auch Byte-Arrays geschrieben werden. In folgendem Beispiel liefert die Methode process()
Byte-Arrays anstatt einzelner Bytes zurück (und null
, wenn keine weiteren Daten folgen):
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName)) {
byte[] bytes;
while ((bytes = process()) != null) {
out.write(bytes);
}
}
Code-Sprache: Java (java)
Diese Methode ist um ein Vielfaches schneller. Schreibt man 10.000.000 mal 10 Bytes (insgesamt die gleiche Menge), benötigt man dafür lediglich 24 Sekunden, also etwas mehr als ein Zehntel der vorherigen Zeit. Schreibt man 1.000.000 mal 100 Bytes, sind es nur noch 2,6 Sekunden, was wiederum etwas mehr als ein Hundertstel der vorherigen Zeit ist.
Relevant ist hier in erster Linie die Anzahl der Schreibvorgänge, nicht die tatsächliche Menge der Daten. Dies liegt daran, dass die Daten blockweise auf den Datenträger geschrieben werden. Dies gilt natürlich nur bis zu einer gewissen Puffergröße. Ein mal zehn Gigabyte zu schreiben ist nicht mehr schneller als zehn mal ein Gigabyte zu schreiben. Der optimale Wert für die Puffergröße hängt von der Hardware als auch der Formatierung des Datenträgers ab. Ich habe mit einem kleinen Testprogramm die Schreibgeschwindigkeit in Abhängigkeit von der Puffergröße gemessen:

Auf meinem System beträgt die optimale Puffergröße 8 KB. Hierbei werden etwa 1.050 MB pro Sekunde erreicht. 8 KB ist auch auf den meisten anderen System die optimale Größe, weshalb Java diesen Wert als Standard nimmt, wie wir im übernächsten Abschnitt sehen werden.
Schreiben von Binärdaten mit dem NIO.2 OutputStream
In Java 7 kam mit Files.newOutputStream()
eine weitere Methode hinzu, um einen OutputStream
zu erzeugen:
String fileName = ...;
try (OutputStream out = Files.newOutputStream(Path.of(fileName))) {
int b;
while ((b = process()) != -1) {
out.write(b);
}
}
Code-Sprache: Java (java)
Diese Methode liefert einen ChannelOutputStream
anstatt eines FileOutputStreams
. Von der Geschwindkeit gibt es sowohl beim Schreiben einzelner Bytes als auch beim Schreiben von Byte-Blöcken auf meinem System keinen relevanten Unterschied gegenüber new FileOutputStream()
.
Schneller schreiben mit dem BufferedOutputStream
Wir haben zuvor festgestellt, dass das Schreiben von Blöcken schneller ist als das Schreiben einzelner Bytes. Diese Tatsache macht sich der BufferedOutputStream
zunutze, in dem er die zu schreibenden Bytes zunächst in einem Puffer zwischenspeichert und diesen erst beim Erreichen einer bestimmten Größe auf den Datenträger schreibt. Dieser Puffer ist standardmäßig 8 KB groß, also genau die Größe, die zu einer optimalen Schreibgeschwindigkeit führt.
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName);
BufferedOutputStream bout = new BufferedOutputStream(out)) {
int b;
while ((b = process()) != -1) {
bout.write(b);
}
}
Code-Sprache: Java (java)
Mit dem BufferedOutputStream
benötigt mein System für das Schreiben von 100.000.000 einzelnen Bytes knapp 250 ms. Das sind etwa 400 MB pro Sekunde. Die 1.050 MB/s aus dem vorherigen Test werden hier aufgrund des Overheads durch das Zwischenspeichern nicht erreicht.
Byte-Arrays schreiben mit dem BufferedOutputStream
Ebenso wie der FileOutputStream
kann auch der BufferedOutputStream
nicht nur einzelne Bytes, sondern auch Byte-Blöcke schreiben:
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName);
BufferedOutputStream bout = new BufferedOutputStream(out)) {
byte[] bytes;
while ((bytes = process()) != null) {
bout.write(bytes);
}
}
Code-Sprache: Java (java)
Diese Methode vereint die Vorteile des Schreibens von Byte-Arrays mit der eines Puffers und liefert quasi immer optimale Schreibgeschwindigkeiten. Beim Schreiben von Binärdaten empfehle ich immer diese Methode zu verwenden.
Schreiben von Textdateien mit dem FileWriter
Um Texte in eine Datei zu schreiben, müssen diese in Binärdaten umgewandelt werden. Das übernimmt der OutputStreamWriter
, den du wie folgt um den FileOutputStream
legst. Die process()
-Methode produziert in folgendem Beispiel einzelne Characters.
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName);
OutputStreamWriter writer = new OutputStreamWriter(out)) {
int c;
while ((c = process()) != -1) {
writer.write(c);
}
}
Code-Sprache: Java (java)
Komfortabler geht es mit dem FileWriter
. Dieser kombiniert FileOutputStream
und OutputStreamWriter
. Der folgende Code ist äquivalent zu dem vorherigen:
String fileName = ...;
try (FileWriter writer = new FileWriter(fileName)) {
int c;
while ((c = process()) != -1) {
writer.write(c);
}
}
Code-Sprache: Java (java)
Der OutputStreamWriter
verwendet intern auch einen 8 KB großen Puffer, so dass das Schreiben von 100.000.000 Zeichen in eine Textdatei etwa 2,5 Sekunden dauert.
Textdateien schneller schreiben mit dem BufferedWriter
Weiter beschleunigen kannst du das Schreiben mit dem BufferedWriter
:
String fileName = ...;
try (FileWriter writer = new FileWriter(fileName);
BufferedWriter bufferedWriter = new BufferedWriter(writer)) {
int c;
while ((c = process()) != -1) {
bufferedWriter.write(c);
}
}
Code-Sprache: Java (java)
Der BufferedWriter
führt einen weiteren 8 KB-Puffer für Zeichen hinzu, welche dann in einem Rutsch kodiert werden, sobald der Puffer geschrieben wird (anstatt Zeichen für Zeichen). Damit reduziert sich die Zeit für das Schreiben von 100.000.000 Zeichen auf ca. 370 ms.
Textdateien schneller schreiben mit dem NIO.2 BufferedWriter
In Java 7 kam mit Files.newBufferedWriter()
eine neue Methode hinzu, um einen BufferedWriter
zu erzeugen:
String fileName = ...;
try (BufferedWriter bufferedWriter = Files.newBufferedWriter(Path.of(fileName))) {
int c;
while ((c = process()) != -1) {
bufferedWriter.write(c);
}
}
Code-Sprache: Java (java)
Die Schreibgeschwindigkeit entspricht auf meinem System der des "klassisch" erstellten BufferedWriters
.
Übersicht Performance Dateien schreiben
Im folgenden Diagramm siehst du noch einmal zusammengefasst, wie lange die vorgestellten Methoden benötigen, um 100.000.000 Bytes bzw. Zeichen in eine Binär- bzw. Textdatei zu schreiben:

Aufgrund des großen Abstands von ungepuffertem zu gepuffertem Schreiben heben sich die gepufferten Methoden hier kaum ab. Das folgende Diagram zeigt daher als Ausschnitt nur die Methoden, die einen Puffer verwenden:

Übersicht FileOutputStream, FileWriter, OutputStreamWriter, BufferedOutputStream, BufferedWriter
Das folgende Diagramm zeigt noch einmal zusammengefasst den Zusammenhang der vorgestellten Klassen zum Schreiben von Binär- und Textdateien aus dem java.io
-Paket:
Durchgezogene Linien stehen für Binärdaten, gestrichtelte für Textdaten. FileWriter
kombiniert FileOutputStream
und OutputStreamWriter
.
Zeichenkodierungen
Das Thema Zeichenkodierung und die Probleme, die damit einhergehen, wurden im vorherigen Artikel ausführlich besprochen.
Im Folgenden beschränke ich mich daher auf diejenigen Aspekte, die für das Schreiben von Textdateien mit Java relevant sind.
Welche Zeichenkodierung verwendet Java standardmäßig zum Schreiben von Textdateien?
Gibt man beim Schreiben einer Textdatei keine Zeichenkodierung an, verwendet Java eine Standard-Codierung. Doch aufgepasst: Welche das ist, hängt zum einen von der verwendeten Methode ab, zum anderen von der eingesetzten Java-Version.
- Die Klassen
FileWriter
undOutputStreamWriter
verwenden internStreamEncoder.forOutputStreamWriter()
. Wird diese Methode ohne Zeichenkodierung aufgerufen, verwendet sieCharset.defaultCharset()
. Diese Methode wiederum liest die Zeichenkodierung aus der System-Property "file.encoding". Ist diese nicht angegeben, wird bis Java 5 standardmäßig ISO-8859-1 verwendet und seit Java 6 UTF-8. - Die Methoden
Files.writeString()
,Files.write()
undFiles.newBufferedWriter()
verwenden alle grundsätzlich UTF-8 als Standardkodierung, ohne die o. g. System Property zu lesen.
Aufgrund dieser Inkonsistenzen sollte man immer die Zeichenkodierung mit angeben. Ich empfehle grundsätzlich UTF-8 zu verwenden. Diese Kodierung wird laut Wikipedia auf 94,3 % aller Webseiten verwendet und kann von daher als De-facto-Standard angesehen werden. Eine Ausnahme besteht natürlich, wenn mit alten Dateien gearbeitet werden muss, die in einer anderen Kodierung geschrieben wurden.
Wie gibt man die Zeichenkodierung beim Schreiben einer Textdatei an?
Im Folgenden findest du für alle bisher besprochenen Methoden ein Beispiel mit Angabe von UTF-8 als Zeichenkodierung:
Files.writeString(path, string, StandardCharsets.UTF_8)
Files.write(path, lines, StandardCharsets.UTF_8)
new FileWriter(file, StandardCharsets.UTF_8)
// diese Methode gibt es erst seit Java 11new InputStreamWriter(outputStream, StandardCharsets.UTF_8)
Files.newBufferedWriter(path, StandardCharsets.UTF_8)
Zusammenfassung und Ausblick
Dieser Artikel hat verschiedene Methoden gezeigt, wie du in Java Byte-Arrays und Strings in Binär- und Textdateien schreiben kannst.
Im dritten Teil der Serie lernst du, wie man die Klassen File
, Path
und Paths
nutzt, um Datei- und Verzeichnispfade zu konstruieren.
In weiteren Artikeln dieser Serie werde ich zeigen:
- Wie liest man den Inhalt eines Verzeichnisse ein?
- Wie kopiert, verschiebt und löscht man Dateien, und wie setzt man Links?
- Wie erstellt man temporäre Dateien?
- Wie verwendet man
DataOutputStream
undDataInputStream
, um strukturierte Daten zu schreiben und zu lesen?
Im weiteren Verlauf der Serie werden dann fortgeschritteren Themen behandelt:
- Die in Java 1.4 eingeführten NIO-Channels und Buffer, um insbesondere das Arbeiten mit großen Dateien zu beschleunigen
- Memory-mapped I/O für rasend schnellen Dateizugriff ohne Streams
- File Locking, um parallel – also aus mehreren Threads oder Prozessen – konfliktfrei auf dieselben Dateien zuzugreifen
Möchtest du informiert werden, wenn weitere Artikel veröffentlicht werden? Dann klicke hier, um dich für den HappyCoders-Newsletter anzumelden. Wenn dir der Artikel gefallen hat, freue ich mich auch, wenn du ihn über einen der Buttons am Ende teilst.