Zum Inhalt springen

Java PrintWriter: println(), printf() und Text in Dateien schreiben

Ein Spritzbeutel aus weißem Backpapier mit silberner Tülle auf Apricot zieht auf einer hellen Platte die fünfte von fünf linksbündigen Zuckergusslinien: darüber Koralle, Gelb, Mint und Lila, unten die himmelblaue Linie, die an der Tülle endet

Ein PrintWriter schreibt Text – Strings, Zahlen, beliebige Objekte – in eine Datei, in einen Netzwerk-Stream oder auf die Konsole. Er umhüllt einen anderen Writer oder einen OutputStream und ergänzt ihn um die Methoden print(), println() und printf(), wie du sie von System.out kennst.

Für das Schreiben in eine Datei sieht der Code in den meisten Fällen so aus:

Path log = Path.of("target/server.log");
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(log))) {
  for (LogEntry entry : LogEntry.SERVER_LOG) {
    out.printf("%s %-5s %s%n", entry.timestamp(), entry.level(), entry.message());
  }
}

Die so geschriebene Datei target/server.log enthält danach diese fünf Zeilen:

2026-10-17 08:15:02 INFO  Server started on port 8080
2026-10-17 08:15:07 INFO  GET /java/java-bufferedreader/ 200
2026-10-17 08:15:09 WARN  GET /java/bufferedreader/ 404
2026-10-17 08:15:11 INFO  GET /java/java-streams/ 200
2026-10-17 08:15:12 ERROR Connection reset by peer

LogEntry.SERVER_LOG ist eine Liste mit den fünf Einträgen der Log-Datei, jeder mit Zeitstempel, Log-Level und Nachricht. Den Record LogEntry mit dieser Konstanten zeigt der Abschnitt Die Beispiele in diesem Artikel.

Files.newBufferedWriter() öffnet die Datei und liefert einen BufferedWriter, der den Text als UTF-8 kodiert. Der PrintWriter um ihn herum schreibt mit printf() jede Zeile nach einem Format-String: %s setzt ein Argument als Text ein, %-5s füllt es linksbündig auf fünf Zeichen auf, und %n beendet die Zeile. Das Try-with-resources schließt den PrintWriter – und damit die Datei – auch dann, wenn beim Schreiben eine Exception auftritt.

PrintWriter gibt es seit Java 1.1. In Java 5 kamen die Konstruktoren für einen Dateinamen und für ein File-Objekt dazu, ebenso printf(), format() und append(). Seit Java 10 nehmen die Konstruktoren die Zeichenkodierung als Charset entgegen, und seit Java 18 schreibt ein PrintWriter, dem du keine Kodierung angibst, UTF-8.

In diesem Artikel erfährst du,

  • was ein PrintWriter selbst tut und was er an die Klassen hinter ihm abgibt,
  • welche Wege es gibt, einen PrintWriter zu erzeugen, welchen ich empfehle und welcher keinen Puffer hat,
  • wie du an eine Datei anhängst, ohne dass die erste Ausführung mit einer Exception abbricht,
  • worin sich print(), println(), write() und append() unterscheiden,
  • wie du mit printf() Spalten ausrichtest und Zahlen mit der passenden Locale formatierst,
  • wann die Zeichen in der Datei ankommen und was autoFlush daran ändert,
  • warum ein PrintWriter keine IOException wirft und wie du Schreibfehler mit checkError() erkennst,
  • wie du mit einem PrintWriter Text in einen String schreibst,
  • worin sich PrintWriter von PrintStream, FileWriter und BufferedWriter unterscheidet,
  • wie schnell ein PrintWriter mit und ohne BufferedWriter dahinter, mit autoFlush und mit printf() schreibt.

Die Beispiele in diesem Artikel

Die meisten Beispiele schreiben die kleine Log-Datei server.log, die der Artikel über den BufferedReader liest. Sie hat fünf Zeilen, jede mit Datum, Uhrzeit, Log-Level und Nachricht. Die Einträge liegen in einem Record:

public record LogEntry(String timestamp, String level, String message) {

  public static final List<LogEntry> SERVER_LOG = List.of(
      new LogEntry("2026-10-17 08:15:02", "INFO", "Server started on port 8080"),
      new LogEntry("2026-10-17 08:15:07", "INFO",
          "GET /java/java-bufferedreader/ 200"),
      new LogEntry("2026-10-17 08:15:09", "WARN", "GET /java/bufferedreader/ 404"),
      new LogEntry("2026-10-17 08:15:11", "INFO", "GET /java/java-streams/ 200"),
      new LogEntry("2026-10-17 08:15:12", "ERROR", "Connection reset by peer"));
}

Den vollständigen Code findest du im GitHub-Repository java-printwriter, im Package eu.happycoders.printwriter. Jeder Abschnitt hat dort eine eigene Klasse mit main()-Methode, die die gezeigten Ausgaben erzeugt. Starte die Klassen im Hauptverzeichnis des Repositorys; alle Dateien, die die Beispiele schreiben, landen im Verzeichnis target/.

Die Beispiele verwenden Sprachmittel aus Java 25 und laufen deshalb erst ab dieser Version. Seit wann es eine Methode oder einen Konstruktor des PrintWriter gibt, steht jeweils im Text.

Was ist ein PrintWriter?

PrintWriter ist ein Writer, der einen anderen Writer umhüllt. Er speichert ihn im Feld out und gibt alles, was du ihm übergibst, als Zeichen an ihn weiter:

  • print(404) wandelt die Zahl mit String.valueOf() in den String "404" und schreibt ihn in out.
  • println(404) schreibt dasselbe und danach das Zeilenende.
  • printf() lässt einen java.util.Formatter den Text zusammensetzen und schreibt die Teile in out.

Die folgende Grafik zeigt, welche Zeichen die drei Methoden nach out schreiben. Jedes Kästchen ist ein eigener Aufruf von out.write():

Was print(), println() und printf() nach out schreiben: print(404) wandelt die Zahl mit String.valueOf() in den String 404; println(404) schreibt 404 und das Zeilenende; printf("Request #%05d%n", 42) lässt einen Formatter Request #, 00042 und das Zeilenende einzeln schreiben – alles in den Writer im Feld out
Der PrintWriter wandelt die Werte in Zeichen um und gibt jedes Stück einzeln an den Writer in out weiter

Zwei Dinge tut der PrintWriter dagegen nicht selbst: Er puffert nicht, und er wandelt keine Zeichen in Bytes um. Beides erledigen die Klassen hinter ihm. Beim Schreiben in eine Datei sind das bis zu drei Klassen, die eine Kette bilden:

  • Ein BufferedWriter sammelt die Zeichen in einem Puffer von 8.192 Zeichen und reicht sie erst weiter, wenn der Puffer voll ist oder geleert wird.
  • Ein OutputStreamWriter kodiert die Zeichen, d. h. er wandelt sie nach einer Zeichenkodierung wie UTF-8 in Bytes um, und schreibt diese in einen OutputStream. Die kodierten Bytes sammelt er dabei in einem eigenen Puffer von bis zu 8.192 Bytes.
  • Ein FileOutputStream – oder ein anderer OutputStream – schreibt die Bytes schließlich in die Datei.

Welche dieser Klassen hinter einem PrintWriter stehen, hängt davon ab, wie du ihn erzeugst:

  • Übergibst du ihm einen Dateinamen, stehen alle drei hinter ihm.
  • Legst du ihn um einen FileWriter, sind es nur OutputStreamWriter und FileOutputStream.
  • Schreibt er z. B. in einen StringWriter, steht keine der drei Klassen hinter ihm.

Die folgende Grafik zeigt die drei Fälle für das Schreiben in Dateien, die ich in den nächsten Abschnitten einzeln vorstelle: links einen PrintWriter um einen BufferedWriter aus Files.newBufferedWriter(), in der Mitte einen PrintWriter, dem du einen Dateinamen übergibst, und rechts einen PrintWriter um einen FileWriter.

Die gestrichelten Pfeile stehen für Textdaten, also Zeichen; die durchgezogenen Pfeile stehen für Binärdaten, also Bytes:

Was hinter einem PrintWriter steht: um Files.newBufferedWriter() ein BufferedWriter, ein OutputStreamWriter und ein OutputStream; mit einem Dateinamen ein BufferedWriter, ein OutputStreamWriter und ein FileOutputStream; um einen FileWriter direkt ein OutputStreamWriter und ein FileOutputStream

Im rechten Fall fehlt der BufferedWriter. Jeder print()-Aufruf erreicht dann sofort den OutputStreamWriter, und der kodiert die Zeichen für jeden Aufruf einzeln. Wie viel Zeit das kostet, zeigt der Abschnitt new PrintWriter(Writer): ohne eigenen Puffer.

Einen PrintWriter erzeugen

PrintWriter hat elf Konstruktoren. Sie lassen sich in drei Gruppen einteilen: Konstruktoren, die einen Writer umhüllen, solche, die einen OutputStream umhüllen, und solche, die selbst eine Datei öffnen – über einen Dateinamen oder ein File-Objekt.

new PrintWriter(Files.newBufferedWriter(…))

Diesen Weg empfehle ich für Dateien. Files.newBufferedWriter() aus der NIO.2-API erwartet einen Path und liefert einen BufferedWriter, den der PrintWriter umhüllt:

Path log = Path.of("target/server.log");
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(log))) {
  out.println("2026-10-17 08:15:02 INFO  Server started on port 8080");
}
System.out.print(Files.readString(log));
2026-10-17 08:15:02 INFO  Server started on port 8080

Die Files.newBufferedWriter()-Variante ohne Charset-Parameter gibt es seit Java 8 und kodiert immer als UTF-8, unabhängig von der Java-Version. Eine andere Kodierung übergibst du als zweites Argument, z. B. Files.newBufferedWriter(log, StandardCharsets.ISO_8859_1). Weitere Argumente sind OpenOptions, mit denen du u. a. an die Datei anhängst – das zeigt der Abschnitt An eine Datei anhängen.

Entscheidend für meine Empfehlung ist ein Unterschied, der sich erst zeigt, wenn ein Zeichen nicht in die gewählte Kodierung passt: Der BufferedWriter aus Files.newBufferedWriter() meldet das mit einer Exception – die anderen Wege ersetzen das Zeichen stillschweigend durch das Ersatzzeichen der Kodierung. Wie der PrintWriter diese Exception an dich weitergibt, steht im Abschnitt Das Euro-Zeichen in ISO-8859-1.

new PrintWriter(String) und new PrintWriter(File)

Seit Java 5 kann ein PrintWriter eine Datei auch selbst öffnen. Dazu übergibst du ihm den Dateinamen als String oder als File-Objekt:

try (PrintWriter out = new PrintWriter("target/greeting.txt")) {
  out.println("Grüße aus Köln");
}
Path greeting = Path.of("target/greeting.txt");
System.out.println(Files.readString(greeting).strip());
System.out.println(Files.size(greeting) + " bytes");
Grüße aus Köln
18 bytes

Der Konstruktor legt dazu intern einen FileOutputStream, einen OutputStreamWriter und einen BufferedWriter an – dieselbe Kette, die du sonst von Hand schreiben würdest. Ohne Kodierungs-Parameter verwendet der OutputStreamWriter die Standard-Kodierung der JVM, und die ist seit Java 18 UTF-8. Deshalb ist die Datei 18 Bytes groß, obwohl „Grüße aus Köln“ nur 14 Zeichen hat: „ü“, „ß“ und „ö“ belegen in UTF-8 je zwei Bytes, und das Zeilenende \n belegt ein weiteres.

Bis einschließlich Java 17 hing die Standard-Kodierung vom Betriebssystem und der Locale ab, auf einem deutschen Windows war sie in der Regel Windows-1252. Details dazu stehen im Artikel über das Schreiben von Dateien, im Abschnitt Welche Zeichenkodierung verwendet Java standardmäßig zum Schreiben von Textdateien?

Eine andere Kodierung gibst du seit Java 10 als Charset an:

try (PrintWriter out = new PrintWriter("target/greeting-latin1.txt", ISO_8859_1)) {
  out.println("Grüße aus Köln");
}
Path latin1 = Path.of("target/greeting-latin1.txt");
System.out.println(Files.readString(latin1, ISO_8859_1).strip());
System.out.println(Files.size(latin1) + " bytes");
Grüße aus Köln
15 bytes

In ISO-8859-1 belegt jeder Umlaut nur ein Byte, die Datei ist deshalb 15 Bytes groß. Bis einschließlich Java 9 gab es dafür nur den Konstruktor, der den Namen der Kodierung als String erwartet, z. B. new PrintWriter(fileName, "ISO-8859-1"). Er wirft eine UnsupportedEncodingException, wenn er den Namen nicht kennt.

Gegenüber Files.newBufferedWriter() haben diese Konstruktoren zwei Nachteile: Sie öffnen die Datei immer zum Überschreiben, und sie ersetzen ein Zeichen, das nicht in die Kodierung passt, ohne Fehlermeldung.

new PrintWriter(Writer): ohne eigenen Puffer

Übergibst du dem PrintWriter einen Writer, umhüllt er genau diesen – und sonst nichts. Er legt keinen BufferedWriter dazwischen. Naheliegend wäre deshalb, ihn um einen FileWriter zu legen, der die Datei öffnet:

try (PrintWriter out = new PrintWriter(new FileWriter("target/server.log"))) {
  // ...
}

Ein FileWriter ist aber nur ein OutputStreamWriter um einen FileOutputStream, wie die rechte Spalte der Grafik oben zeigt. Jeder print()-, println()- und printf()-Aufruf geht deshalb direkt an den OutputStreamWriter.

Wie viele Aufrufe das sind, zeigt ein zählender Writer hinter dem PrintWriter. Das folgende Programm schreibt die fünf Log-Zeilen einmal direkt in diesen Zähler und einmal über einen BufferedWriter dazwischen:

CountingWriter direct = new CountingWriter(Writer.nullWriter());
try (PrintWriter out = new PrintWriter(direct)) {
  writeLog(out);
}
System.out.println("Without BufferedWriter: " + direct.calls + " write calls");

CountingWriter buffered = new CountingWriter(Writer.nullWriter());
try (PrintWriter out = new PrintWriter(new BufferedWriter(buffered))) {
  writeLog(out);
}
System.out.println("With BufferedWriter:    " + buffered.calls + " write call(s)");
Without BufferedWriter: 34 write calls
With BufferedWriter:    1 write call(s)

CountingWriter ist ein FilterWriter aus dem Beispiel-Repository, der jeden Aufruf seiner write()-Methoden zählt und ihn an den umhüllten Writer weitergibt – hier an Writer.nullWriter() (seit Java 11), der alle Zeichen verwirft. writeLog() ist die printf()-Schleife aus der Einleitung.

Ohne BufferedWriter kommen beim Writer dahinter 34 Aufrufe für fünf Zeilen an. Der Formatter hinter printf() schreibt jedes Argument, jedes Leerzeichen dazwischen und das Zeilenende einzeln, und %-5s hängt sogar jedes Füll-Leerzeichen mit einem eigenen Aufruf an. Mit BufferedWriter landen alle 34 Teile in dessen Puffer, und beim Schließen gibt er sie mit einem einzigen Aufruf weiter.

Die folgende Grafik zeigt die 34 Teile Zeile für Zeile; jedes Kästchen ist ein write()-Aufruf. Blau umrandet sind Zeitstempel, Log-Level und Nachricht, grau umrandet die Leerzeichen (␣) und die Zeilenenden (\n). Oben gibt der PrintWriter jedes Teil einzeln an den FileWriter weiter, unten sammelt der BufferedWriter alle Teile und gibt sie mit einem Aufruf weiter:

Die 34 write()-Aufrufe für fünf Log-Zeilen: ohne BufferedWriter gehen Zeitstempel, Leerzeichen, Log-Level, Füll-Leerzeichen, Leerzeichen, Nachricht und Zeilenende jeder Zeile einzeln an den FileWriter, 34 Aufrufe; mit BufferedWriter liegen dieselben Teile in dessen Puffer und gehen mit einem Aufruf an den FileWriter

Steht hinter dem PrintWriter direkt ein FileWriter, kodiert dieser die Zeichen jedes der 34 Aufrufe einzeln. Mit einem BufferedWriter dazwischen bekommt er dagegen Blöcke von bis zu 8.192 Zeichen – hier alle fünf Zeilen in einem Aufruf – und kodiert jeden Block in einem Durchgang.

Teurer als das einzelne Kodieren ist eine Folge der kleinen Aufrufe: Der OutputStreamWriter legt seinen Byte-Puffer seit Java 19 initial mit nur 512 Bytes an und vergrößert ihn nur, wenn ein einzelner Aufruf mehr Zeichen an ihn übergibt, als kodiert sicher in diesen Puffer hineinpassen – bei UTF-8 ab 171 Zeichen (170 Zeichen belegen bis zu 510 Bytes, 171 Zeichen bis zu 513 Bytes, also mehr als 512). Bei vielen kurzen Aufrufen mit maximal 170 Zeichen schreibt er die Datei deshalb in Blöcken von 512 statt 8.192 Bytes. Wie viel Zeit das kostet, zeigt der Abschnitt Wie schnell ist der PrintWriter?

Legst du einen PrintWriter um einen FileWriter, dann also besser mit einem BufferedWriter dazwischen – oder du nimmst gleich Files.newBufferedWriter().

new PrintWriter(OutputStream): ein PrintWriter um einen beliebigen OutputStream

Nicht jeder Text geht in eine Datei. Schreibst du in einen Netzwerk-Socket, in die Eingabe eines Prozesses oder in eine komprimierte Datei, liefert dir die jeweilige API einen OutputStream. Den übergibst du dem PrintWriter direkt: Er legt dann selbst einen OutputStreamWriter und einen BufferedWriter darum, so wie bei einem Dateinamen.

Das folgende Beispiel schreibt die Log-Zeilen in eine mit gzip komprimierte Datei. Zur Kontrolle liest es die erste Zeile mit einem BufferedReader wieder ein und zählt die Zeilen:

Path gzipped = Path.of("target/server.log.gz");
try (PrintWriter out = new PrintWriter(
    new GZIPOutputStream(Files.newOutputStream(gzipped)), false, UTF_8)) {
  for (LogEntry entry : LogEntry.SERVER_LOG) {
    out.printf("%s %-5s %s%n", entry.timestamp(), entry.level(), entry.message());
  }
}

try (BufferedReader reader = new BufferedReader(new InputStreamReader(
    new GZIPInputStream(Files.newInputStream(gzipped)), UTF_8))) {
  System.out.println(reader.readLine());
  System.out.println(reader.lines().count() + 1 + " lines");
}
2026-10-17 08:15:02 INFO  Server started on port 8080
5 lines

Die Kette liest sich von außen nach innen: Der PrintWriter formatiert die Zeilen, sein interner OutputStreamWriter kodiert sie als UTF-8, GZIPOutputStream komprimiert die Bytes, und der OutputStream aus Files.newOutputStream() schreibt sie in die Datei.

Das zweite Argument ist autoFlush (hier false), das der Abschnitt Wann die Zeichen in der Datei ankommen: flush() und autoFlush erklärt. Den im Beispiel verwendeten Konstruktor mit dem dritten Argument, dem Charset, gibt es seit Java 10. Ohne Angabe eines Charset kodiert der PrintWriter mit der Standard-Kodierung der JVM – es sei denn, der OutputStream ist ein PrintStream. Dessen Kodierung übernimmt der PrintWriter seit Java 18, wie der Abschnitt PrintWriter auf der Konsole und im Servlet zeigt.

Du solltest bei einem OutputStream die Kodierung trotzdem immer explizit angeben. Welche Kodierung die Bytes haben müssen, sollte der Empfänger bestimmen – das Protokoll, der Prozess, das Programm, das die Datei später liest –, nicht die JVM, die sie schreibt.

An eine Datei anhängen

Alle bisher gezeigten Wege überschreiben eine vorhandene Datei. Um stattdessen an ihr Ende zu schreiben, übergibst du an Files.newBufferedWriter() die Option StandardOpenOption.APPEND. Naheliegend wäre, nur diese eine Option anzugeben – das scheitert aber, solange die Datei noch nicht existiert:

Path log = Path.of("target/append.log");
Files.deleteIfExists(log);

try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(log, APPEND))) {
  out.println("first run");
} catch (NoSuchFileException e) {
  System.out.println(e);
}
java.nio.file.NoSuchFileException: target/append.log

Sobald du an Files.newBufferedWriter() irgendeine Option übergibst, gelten nur noch die übergebenen Optionen (plus WRITE). Ohne Optionen arbeitet die Methode so, als hättest du CREATE, TRUNCATE_EXISTING und WRITE angegeben. CREATE musst du deshalb ausdrücklich mitgeben.

Files.newBufferedWriter() nimmt die Optionen als Varargs-Parameter (OpenOption... options) entgegen. Du schreibst sie deshalb einfach durch Kommas getrennt hinter den Path:

for (String run : new String[] {"first run", "second run"}) {
  try (PrintWriter out =
      new PrintWriter(Files.newBufferedWriter(log, CREATE, APPEND))) {
    out.println(run);
  }
}
System.out.print(Files.readString(log));
first run
second run

Mit diesen Optionen legt der erste Durchlauf die Datei an, der zweite Durchlauf hängt seine Zeile an.

Wenn du statt Files.newBufferedWriter() einen FileWriter über seinen Konstruktor erzeugst, setzt du zum Anhängen an eine Datei den Parameter append auf true. Im folgenden Beispiel ist das das letzte Argument.

Damit der PrintWriter gepuffert schreibt, legst du selbst einen BufferedWriter dazwischen:

try (PrintWriter out = new PrintWriter(new BufferedWriter(
    new FileWriter(log.toFile(), UTF_8, true)))) {
  out.println("third run");
}
System.out.print(Files.readString(log));
first run
second run
third run

Der FileWriter legt die Datei bei Bedarf selbst an, eine fehlende Datei ist hier also kein Problem. Den Konstruktor mit Charset und append gibt es seit Java 11.

PrintWriter hat zwei Arten von Schreibmethoden. Seine eigenen – print(), println() und printf() – wandeln Werte in Text um; printf() zeigt der Abschnitt Formatiert schreiben mit printf() und format(). Die von Writer geerbten – write() und append() – schreiben Zeichen, so wie bei jedem Writer.

Werte ausgeben mit print() und println()

print() gibt es für boolean, char, int, long, float, double, char[], String und Object. println() gibt es mit denselben Parametertypen und zusätzlich ohne Parameter. Es schreibt dasselbe wie print() und danach ein Zeilenende.

Das folgende Beispiel schreibt eine Zahl, eine Kommazahl, ein Boolean und eine Liste in eine Zeile, danach einen String, der null ist, eine leere Zeile und das Wort „end“:

Path file = Path.of("target/print.txt");
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) {
  out.print(404);
  out.print(' ');
  out.print(3.5);
  out.print(' ');
  out.print(true);
  out.print(' ');
  out.println(List.of("GET", "POST"));
  String missing = null;
  out.println(missing);
  out.println();
  out.println("end");
}
System.out.print(Files.readString(file));
404 3.5 true [GET, POST]
null

end

print(3.5) schreibt „3.5“ mit Punkt, unabhängig von der Locale, denn print() wandelt jeden Wert mit String.valueOf() um. Für die Liste ruft String.valueOf() deren toString()-Methode auf. Für den String, der null ist, schreibt println() den Text „null“, und println() ohne Argument schreibt nur das Zeilenende – daher die leere Zeile vor „end“.

Das Zeilenende

println() schreibt nicht das Zeichen \n, sondern den Wert von System.lineSeparator(). Dieser Wert hängt vom Betriebssystem ab: Auf Linux und macOS ist er \n, auf Windows \r\n. Das folgende Programm gibt den Inhalt der Datei aus dem vorigen Beispiel aus, mit sichtbar gemachten Steuerzeichen:

String text = Files.readString(file);
System.out.println(text.replace("\r", "\\r").replace("\n", "\\n"));

Auf macOS liefert es:

404 3.5 true [GET, POST]\nnull\n\nend\n

Auf Windows stünde hinter jeder Zeile \r\n.

Auch %n in einem Format-String und newLine() im BufferedWriter geben den Zeilenseparator des Betriebssystems aus.

Für eine Datei, die auf demselben System gelesen wird, ist das Zeilenende des Betriebssystems richtig. Schreibst du dagegen ein Format, das ein bestimmtes Zeilenende vorschreibt – HTTP-Header und CSV nach RFC 4180 beispielsweise verlangen \r\n –, darfst du println() nicht verwenden. Schreib das Zeilenende dann selbst mit print("\r\n").

write(int) ist nicht print(int)

write() kommt aus der Klasse Writer und schreibt Zeichen, keine Werte. write(int) deutet seinen Parameter deshalb als Zeichencode, print(int) als Zahl:

try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) {
  out.write(65);
  out.println();
  out.print(65);
  out.println();
}
System.out.print(Files.readString(file));
A
65

write(65) schreibt das Zeichen mit dem Code 65, das „A“, print(65) die Ziffern „6“ und „5“. Für Strings und char-Arrays schreiben write() und print() dagegen dieselben Zeichen.

append()

append() stammt aus dem Interface Appendable und nimmt eine CharSequence oder ein einzelnes char entgegen. Anders als print() gibt append() den PrintWriter zurück, du kannst die Aufrufe also verketten:

try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) {
  out.append("GET ").append("/java/java-streams/").append(' ').append("200")
      .println();
}
System.out.print(Files.readString(file));
GET /java/java-streams/ 200

Die Kette endet mit println(), denn println() gibt nichts zurück.

null ausgeben

print(String) und print(Object) schreiben für null den Text „null“, wie das Beispiel oben zeigt. print(char[]) dagegen wirft für null eine NullPointerException:

try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) {
  char[] chars = null;
  out.print(chars);
} catch (NullPointerException e) {
  System.out.println(e);
}
java.lang.NullPointerException: Cannot read the array length because "buf" is null

Formatiert schreiben mit printf() und format()

printf() setzt eine Zeile nach einem Format-String zusammen. Jeder Platzhalter darin beginnt mit % und endet mit einem Buchstaben, der die Umwandlung angibt; dazwischen stehen optional Flags, eine Breite und eine Genauigkeit. Das folgende Beispiel zeigt die Platzhalter, die du für Log-Zeilen und Tabellen am häufigsten brauchst:

try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) {
  out.printf("%s %-5s %s%n", "2026-10-17 08:15:12", "ERROR",
      "Connection reset by peer");
  out.printf("[%-5s] [%5s]%n", "WARN", "WARN");
  out.printf("%d of %d requests failed (%d %%)%n", 1, 4, 25);
  out.printf("Request #%05d%n", 42);
}
System.out.print(Files.readString(file));
2026-10-17 08:15:12 ERROR Connection reset by peer
[WARN ] [ WARN]
1 of 4 requests failed (25 %)
Request #00042

Die Platzhalter im Einzelnen:

  • %s schreibt ein Argument als Text, %d eine Ganzzahl.
  • Eine Zahl zwischen % und dem Buchstaben ist die Mindestbreite: %5s füllt „WARN“ links mit Leerzeichen auf fünf Zeichen auf, steht also rechtsbündig. Mit dem Flag - wird daraus %-5s, und der Text steht linksbündig.
  • Das Flag 0 füllt mit Nullen statt mit Leerzeichen: %05d schreibt die 42 als „00042“.
  • %% schreibt das Prozentzeichen selbst, %n das Zeilenende des Betriebssystems, wie println().

Die vollständige Syntax beschreibt die Javadoc der Klasse java.util.Formatter.

format() tut dasselbe wie printf(): printf() ruft format() auf. Beide geben den PrintWriter zurück, du kannst sie also verketten:

try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) {
  out.printf("%d ", 1).printf("%d ", 2).format("%d%n", 3);
}
System.out.print(Files.readString(file));
1 2 3

Zahlen und die Locale

Ohne Locale-Argument formatiert printf() Zahlen nach der Standard-Locale der JVM, die diese aus den Einstellungen des Betriebssystems übernimmt. Auf meinem Rechner ist das en_DE – englische Sprache, deutsche Zahlenformate. Das folgende Beispiel schreibt dieselbe Kommazahl mit der Standard-Locale, mit drei ausdrücklich angegebenen Locales und zum Vergleich mit print():

System.out.println("Default locale: " + Locale.getDefault());
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) {
  out.printf("%.2f ms%n", 1234.5);
  out.printf(Locale.GERMANY, "%,.2f ms%n", 1234.5);
  out.printf(Locale.US, "%,.2f ms%n", 1234.5);
  out.printf(Locale.ROOT, "%.2f ms%n", 1234.5);
  out.print(1234.5);
  out.println(" ms");
}
System.out.print(Files.readString(file));
Default locale: en_DE
1234,50 ms
1.234,50 ms
1,234.50 ms
1234.50 ms
1234.5 ms

%.2f schreibt eine Kommazahl mit zwei Nachkommastellen, das Flag , fügt Tausendertrennzeichen ein. Welche Zeichen dabei als Dezimal- und Tausendertrennzeichen erscheinen, bestimmt die Locale. Der erste printf()-Aufruf mit der Standard-Locale en_DE und der zweite mit Locale.GERMANY setzen ein Komma vor die Nachkommastellen, Locale.US und Locale.ROOT einen Punkt. print() hängt an keiner Locale, sondern setzt immer einen Punkt als Dezimaltrennzeichen: Es wandelt die Zahl über String.valueOf() mit Double.toString() um, und dessen Format schreibt den Punkt fest vor.

Schreibst du eine Datei, die ein anderes Programm einliest – eine CSV-Datei, ein Log, eine Konfiguration –, gib deshalb Locale.ROOT an. Sonst schreibt dasselbe Programm auf einem Rechner mit anderen Einstellungen ein Komma, wo das lesende Programm einen Punkt erwartet.

Ein fehlendes Argument

Einen Fehler im Format-String meldet printf() mit einer Exception. Das folgende Beispiel übergibt für drei Platzhalter nur zwei Argumente:

try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) {
  out.printf("%s %-5s %s%n", "2026-10-17 08:15:12", "ERROR");
} catch (IllegalArgumentException e) {
  System.out.println(e);
}
System.out.println("In the file: \"" + Files.readString(file) + "\"");
java.util.MissingFormatArgumentException: Format specifier '%s'
In the file: "2026-10-17 08:15:12 ERROR "

Die MissingFormatArgumentException ist eine Unterklasse von IllegalArgumentException und damit eine unchecked Exception. Beachte die Datei: Was vor dem dritten Platzhalter steht, hat der Formatter schon geschrieben, bevor er das fehlende Argument bemerkt hat.

Fehler im Format-String reicht der PrintWriter also an dich weiter. Fehler beim Schreiben dagegen nicht – davon handelt der Abschnitt Schreibfehler erkennen mit checkError().

Wann die Zeichen in der Datei ankommen: flush() und autoFlush

Ein PrintWriter um einen BufferedWriter schreibt zunächst nur in dessen Puffer. In der Datei kommen die Zeichen erst an, wenn der Puffer voll ist, wenn du flush() aufrufst oder wenn du den PrintWriter schließt. Das folgende Beispiel schreibt eine Zeile und fragt danach die Größe der Datei ab:

Path file = Path.of("target/flush.log");
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) {
  out.println("2026-10-17 08:15:02 INFO  Server started on port 8080");
  System.out.println("After println(): " + Files.size(file) + " bytes");
  out.flush();
  System.out.println("After flush():   " + Files.size(file) + " bytes");
}
After println(): 0 bytes
After flush():   54 bytes

Nach println() ist die Datei noch leer. Erst flush() gibt die Zeichen durch alle Schichten bis zum Betriebssystem weiter: Der PrintWriter ruft flush() auf dem BufferedWriter auf. Der BufferedWriter leert seinen Puffer in den OutputStreamWriter, und dieser schreibt die kodierten Bytes schließlich in die Datei.

Mit dem zweiten Konstruktorparameter, autoFlush, ruft der PrintWriter flush() selbst auf – aber nur nach println(), printf() und format(), nicht nach print():

try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file), true)) {
  out.print("2026-10-17 08:15:07 ");
  System.out.println("After print():      " + Files.size(file) + " bytes");
  out.println("INFO  GET /java/java-bufferedreader/ 200");
  System.out.println("After println():    " + Files.size(file) + " bytes");
  out.print("2026-10-17 08:15:09 WARN  GET /java/bufferedreader/ 404\n");
  System.out.println("After print(...\\n): " + Files.size(file) + " bytes");
  out.printf("%s %-5s %s%n", "2026-10-17 08:15:11", "INFO",
      "GET /java/java-streams/ 200");
  System.out.println("After printf():     " + Files.size(file) + " bytes");
}
After print():      0 bytes
After println():    61 bytes
After print(...\n): 61 bytes
After printf():     171 bytes

Beachte die dritte Zeile: Auch ein print(), dessen Text mit \n endet, löst kein flush() aus. Die WARN-Zeile kommt erst mit dem folgenden printf() in der Datei an.

autoFlush lohnt sich, wenn ein anderes Programm mitliest, während du schreibst – z. B. auf der Konsole, bei einem zeilenweisen Netzwerkprotokoll oder bei einer Log-Datei, die jemand mit tail -f verfolgt. Für eine Datei, die erst nach dem Schreiben gelesen wird, kostet autoFlush nur Zeit: In der Messung im Abschnitt Wie schnell ist der PrintWriter? braucht ein PrintWriter mit autoFlush für die Testdatei 17-mal so lange wie ohne.

Den Parameter autoFlush haben nur die PrintWriter-Konstruktoren, die einen Writer oder einen OutputStream umhüllen. Ein PrintWriter, dem du einen Dateinamen oder ein File-Objekt übergibst, leert seinen Puffer nie von selbst.

Schreibfehler erkennen mit checkError()

Keine Methode des PrintWriter wirft eine IOException – nur seine Konstruktoren tun das, etwa eine FileNotFoundException, wenn sie die Datei nicht anlegen können. Tritt beim Schreiben ein Fehler auf, fängt der PrintWriter die Exception ab und merkt sich nur, dass ein Fehler aufgetreten ist. Abfragen kannst du das mit checkError().

Ein Writer, der Fehler wirft

Das folgende Beispiel legt den PrintWriter um einen Writer, der bei jedem Schreibzugriff eine IOException wirft – so, wie es ein Writer auf einer vollen Festplatte oder um eine abgebrochene Netzwerkverbindung tut:

PrintWriter out = new PrintWriter(new FailingWriter());
out.println("2026-10-17 08:15:02 INFO  Server started on port 8080");
System.out.println("println() returned normally");
System.out.println("checkError(): " + out.checkError());
out.close();
println() returned normally
checkError(): true

FailingWriter ist eine kleine Klasse aus dem Beispiel-Repository, deren write()- und flush()-Methoden immer eine IOException werfen. println() kehrt trotzdem normal zurück, erst checkError() liefert true. Welche Exception es war und was sie gemeldet hat, erfährst du nicht.

Bevor checkError() den Fehlerstatus liefert, ruft es flush() auf. Ein Fehler, der erst beim Leeren der Puffer auftritt, fällt so ebenfalls auf.

Schreiben nach close()

Auch ein Schreibzugriff auf einen geschlossenen PrintWriter wirft keine Exception:

PrintWriter closed = new PrintWriter(Writer.nullWriter());
closed.close();
closed.println("too late");
System.out.println("checkError(): " + closed.checkError());
checkError(): true

Das Euro-Zeichen in ISO-8859-1

Die Kodierung ISO-8859-1 hat kein Euro-Zeichen. Das folgende Beispiel schreibt „Preis: 5 €“ trotzdem als ISO-8859-1 – einmal mit dem PrintWriter-Konstruktor für Dateien und einmal über Files.newBufferedWriter(), jeweils in eine eigene Datei:

Path first = Path.of("target/price-file-constructor.txt");
try (PrintWriter latin1 = new PrintWriter(first.toFile(), ISO_8859_1)) {
  latin1.println("Preis: 5 €");
  System.out.println("new PrintWriter(File, ISO_8859_1), checkError(): "
      + latin1.checkError());
}
System.out.println("In the first file:  "
    + Files.readString(first, ISO_8859_1).strip());
System.out.println();

Path second = Path.of("target/price-new-buffered-writer.txt");
try (PrintWriter latin1 =
    new PrintWriter(Files.newBufferedWriter(second, ISO_8859_1))) {
  latin1.println("Preis: 5 €");
  System.out.println("Files.newBufferedWriter(), checkError():          "
      + latin1.checkError());
}
System.out.println("In the second file: \""
    + Files.readString(second, ISO_8859_1) + "\"");
new PrintWriter(File, ISO_8859_1), checkError(): false
In the first file:  Preis: 5 ?

Files.newBufferedWriter(), checkError():          true
In the second file: "Preis: 5 Preis: 5 "

Der PrintWriter, der die Datei selbst öffnet (der erste PrintWriter im Beispiel oben), ersetzt das Euro-Zeichen durch ein Fragezeichen und meldet keinen Fehler. Sein OutputStreamWriter ersetzt Zeichen, die die Kodierung nicht kennt, stillschweigend durch das Ersatzzeichen der Kodierung – bei ISO-8859-1 ist das „?“.

Der BufferedWriter aus Files.newBufferedWriter() dagegen wirft beim Kodieren des Euro-Zeichens eine UnmappableCharacterException, eine Unterklasse von IOException. Der PrintWriter (der zweite im Beispiel oben) fängt sie ab, und checkError() liefert true.

In der zweiten Datei steht danach „Preis: 5 “ – zweimal und ohne Zeilenende. Den ersten Versuch, den Puffer zu leeren, unternimmt checkError(), den zweiten close(). Beide Male kodiert der OutputStreamWriter die Zeichen vor dem Euro-Zeichen in seinen Byte-Puffer und bricht dann ab. Der BufferedWriter hat seinen Puffer dabei nicht geleert und liefert beim zweiten Versuch denselben Text noch einmal. Beim Schließen schreibt der OutputStreamWriter schließlich alles, was in seinem Byte-Puffer steht, in die Datei.

Liefert checkError() true, ist der Inhalt der Datei also nicht nur unvollständig, sondern kann auch Teile doppelt enthalten. Ich empfehle deshalb, checkError() als letzte Anweisung im Try-Block aufzurufen und die Datei bei true zu verwerfen. Musst du wissen, welcher Fehler aufgetreten ist, nimm statt des PrintWriter einen BufferedWriter: Er reicht jede IOException an dich weiter.

Einen PrintWriter schließen

Ein PrintWriter hält, über die Klassen hinter ihm, eine Datei oder eine Netzwerkverbindung offen. Schließt du den PrintWriter nicht, bleiben Datei oder Netzwerkverbindung offen – und außerdem bleiben die Zeichen in seinem BufferedWriter liegen. Das folgende Programm schreibt die fünf Log-Zeilen und vergisst, den PrintWriter zu schließen:

Path log = Path.of("target/not-closed.log");
PrintWriter out = new PrintWriter(Files.newBufferedWriter(log));
for (LogEntry entry : LogEntry.SERVER_LOG) {
  out.printf("%s %-5s %s%n", entry.timestamp(), entry.level(), entry.message());
}
System.out.println("Bytes in the file at the end of main(): " + Files.size(log));
Bytes in the file at the end of main(): 0

Auch nachdem das Programm beendet ist, bleibt die Datei leer: Die JVM leert beim Beenden keine Puffer von Writern. Die fünf Zeilen, 276 Zeichen, haben den BufferedWriter nie verlassen.

Schließe den PrintWriter deshalb grundsätzlich mit Try-with-resources. Dabei reicht es, den äußersten Writer zu schließen: close() leert den Puffer und schließt den Writer, den der PrintWriter umhüllt, und dieser schließt wiederum die Klasse, die er umhüllt. Das zeigt das folgende Beispiel mit einem Writer, der beim Schließen eine Meldung ausgibt:

try (PrintWriter out =
    new PrintWriter(new ClosingReporter(Writer.nullWriter()))) {
  out.println("2026-10-17 08:15:02 INFO  Server started on port 8080");
}
System.out.println("After the try block");
Inner writer closed
After the try block

ClosingReporter ist ein FilterWriter aus dem Beispiel-Repository, der in close() die Meldung ausgibt und dann den umhüllten Writer schließt.

Die Ausnahme ist System.out. Schließt du einen PrintWriter um System.out, schließt er auch System.out, und keine Ausgabe über System.out erscheint danach noch auf der Konsole:

PrintWriter console = new PrintWriter(System.out, true);
console.println("Via the PrintWriter");
console.close();
System.out.println("Via System.out after close()");
System.err.println("System.out.checkError(): " + System.out.checkError());
Via the PrintWriter
System.out.checkError(): true

Die Zeile „Via System.out after close()“ fehlt, die über System.err dagegen erscheint. System.out ist ein PrintStream und wirft – wie der PrintWriter – keine IOException. Erst checkError() zeigt, dass die Ausgabe gescheitert ist. Einen PrintWriter um System.out solltest du deshalb nicht schließen. Es reicht, ihn mit flush() zu leeren oder mit autoFlush anzulegen.

Text in einen String schreiben: PrintWriter und StringWriter

Ein StringWriter sammelt alle Zeichen, die er bekommt, in einem StringBuffer; toString() liefert sie als String. Legst du einen PrintWriter um ihn, schreibst du mit println() und printf() in einen String statt in eine Datei. Das folgende Beispiel sammelt die Log-Zeilen, deren Level nicht INFO ist:

StringWriter buffer = new StringWriter();
try (PrintWriter out = new PrintWriter(buffer)) {
  for (LogEntry entry : LogEntry.SERVER_LOG) {
    if (!entry.level().equals("INFO")) {
      out.printf("%-5s %s%n", entry.level(), entry.message());
    }
  }
  System.out.print(buffer);
}
WARN  GET /java/bufferedreader/ 404
ERROR Connection reset by peer

Der Text steht schon vor dem Ende des Try-Blocks im StringWriter, ohne flush(): Ein PrintWriter, dem du einen Writer übergibst, hat keinen eigenen Puffer, wie der Abschnitt new PrintWriter(Writer): ohne eigenen Puffer zeigt.

Du brauchst diese Kombination, wenn eine API einen PrintWriter erwartet. So schreibt z. B. Throwable.printStackTrace() den Stacktrace ohne Argument nach System.err, mit einem PrintWriter als Argument dorthin, wohin dieser schreibt – hier in einen String:

try {
  Integer.parseInt("8O80");
} catch (NumberFormatException e) {
  StringWriter trace = new StringWriter();
  try (PrintWriter out = new PrintWriter(trace)) {
    e.printStackTrace(out);
  }
  String text = trace.toString();
  System.out.println(text.lines().findFirst().orElseThrow());
  System.out.println(text.lines().count() + " lines");
}
java.lang.NumberFormatException: For input string: "8O80"
5 lines

Den String kannst du dann z. B. in eine Fehlermeldung oder eine Datenbank schreiben. „8O80“ enthält übrigens ein großes O statt einer Null.

Baust du nur selbst einen String zusammen, brauchst du keinen PrintWriter. String.formatted() (seit Java 15) versteht dieselben Format-Strings wie printf(), und ein StringBuilder sammelt die Teile:

StringBuilder text = new StringBuilder();
for (LogEntry entry : LogEntry.SERVER_LOG) {
  if (!entry.level().equals("INFO")) {
    text.append("%-5s %s%n".formatted(entry.level(), entry.message()));
  }
}
System.out.print(text);
WARN  GET /java/bufferedreader/ 404
ERROR Connection reset by peer

PrintWriter auf der Konsole und im Servlet

System.out ist ein PrintStream, kein PrintWriter. Brauchst du für eine Methode einen PrintWriter, der auf die Konsole schreibt, legst du ihn um System.out, mit autoFlush, damit jede Zeile sofort erscheint:

System.out.println("System.out encodes as " + System.out.charset());

PrintWriter console = new PrintWriter(System.out, true);
console.println("Grüße aus Köln");
console.printf("%-5s %s%n", "WARN", "GET /java/bufferedreader/ 404");
System.out encodes as UTF-8
Grüße aus Köln
WARN  GET /java/bufferedreader/ 404

Seit Java 18 übernimmt ein PrintWriter, den du um einen PrintStream legst, dessen Kodierung – hier UTF-8. Bis einschließlich Java 17 kodierte er mit der Standard-Kodierung der JVM, auch wenn System.out eine andere verwendete. Den PrintWriter solltest du nicht schließen, wie der Abschnitt Einen PrintWriter schließen zeigt.

Zwei APIs liefern dir fertige PrintWriter:

  • System.console().writer() liefert einen PrintWriter auf die Konsole, wenn das Programm eine hat. Hat es keine, liefert schon System.console() null.
  • In einem Servlet liefert response.getWriter() einen PrintWriter, der in die HTTP-Antwort schreibt.

IO.println() aus Java 25 dagegen schreibt über System.out, nicht über einen PrintWriter.

PrintWriter vs. PrintStream

PrintStream hat dieselben print()-, println()- und printf()-Methoden wie PrintWriter. Der Unterschied liegt in der Basisklasse: PrintStream ist ein OutputStream und kann deshalb auch Bytes schreiben, PrintWriter ist ein Writer und schreibt nur Zeichen.

Unterschiedlich ist auch, wann autoFlush leert. Das folgende Programm legt einen PrintStream und einen PrintWriter, beide mit autoFlush, um einen OutputStream, der zählt, wie oft flush() aufgerufen wird, und schreibt in beide dasselbe:

FlushCounter streamCounter = new FlushCounter();
FlushCounter writerCounter = new FlushCounter();
PrintStream stream = new PrintStream(streamCounter, true, UTF_8);
PrintWriter writer = new PrintWriter(writerCounter, true, UTF_8);

stream.print("first line\nsecond line\n");
writer.print("first line\nsecond line\n");
System.out.println("print(\"...\\n...\\n\"): PrintStream " + streamCounter.flushes
    + " flush(es), PrintWriter " + writerCounter.flushes);

stream.print('x');
writer.print('x');
System.out.println("print('x'):          PrintStream " + streamCounter.flushes
    + " flush(es), PrintWriter " + writerCounter.flushes);

stream.println("third line");
writer.println("third line");
System.out.println("println(...):        PrintStream " + streamCounter.flushes
    + " flush(es), PrintWriter " + writerCounter.flushes);
print("...\n...\n"): PrintStream 2 flush(es), PrintWriter 0
print('x'):          PrintStream 3 flush(es), PrintWriter 0
println(...):        PrintStream 5 flush(es), PrintWriter 1

Die Zähler sind kumuliert. Der PrintWriter leert genau einmal, beim println(). Der PrintStream dagegen leert bei jedem der drei Aufrufe, auch bei print('x'), das gar kein Zeilenende enthält.

Der Grund liegt in seinem Aufbau: Ein PrintStream kodiert jeden Text mit einem internen OutputStreamWriter, der die Bytes an die eigene write(byte[], int, int)-Methode des PrintStream gibt – und diese leert bei autoFlush nach jedem Byte-Array. Enthält der Text ein \n, leert print() danach noch einmal. Deshalb zählt der Zähler beim ersten Aufruf zwei Flushes und beim println() ebenfalls zwei.

Die folgende Tabelle stellt die beiden Klassen gegenüber:

PrintWriterPrintStream
BasisklasseWriterOutputStream
Bytes schreiben✗write(int), write(byte[])
autoFlush leert nachprintln(), printf(), format()jedem Text und Byte-Array,
write(int) nur bei \n
IOExceptionwird abgefangen, abrufbar mit checkError()wird abgefangen, abrufbar mit checkError()
Bekannte Exemplareresponse.getWriter(),
System.console().writer()
System.out, System.err
Testdatei schreiben121 ms141 ms

Meine Empfehlung: Schreib Text mit einem PrintWriter. Einen PrintStream brauchst du, wo eine API einen erwartet – z. B. System.setOut() – oder wo du Text und Bytes in denselben Stream schreibst.

PrintWriter vs. FileWriter vs. BufferedWriter

PrintWriter, FileWriter und BufferedWriter sind keine Alternativen, zwischen denen du wählst, sondern Schichten, die du übereinanderlegst. Jede hat eine Aufgabe:

FileWriterBufferedWriterPrintWriter
AufgabeDatei öffnen, Zeichen kodierenZeichen puffernWerte als Text schreiben, formatieren
Puffer512 bis 8.192 Bytes
(kodierte Bytes)
8.192 Zeichenkeiner
print(), println(), printf()✗✗
(nur write() und newLine())
✓
IOExceptionwird geworfenwird geworfenwird abgefangen
An Datei anhängenKonstruktorparameter appendFiles.newBufferedWriter()
mit CREATE und APPEND
✗
(nur über den Writer dahinter)

Daraus ergeben sich drei Empfehlungen:

  • Für Text mit println() und printf() nimm new PrintWriter(Files.newBufferedWriter(path)).
  • Musst du jeden Schreibfehler als Exception sehen, nimm Files.newBufferedWriter(path) allein und schreib mit write() und newLine().
  • Einen FileWriter brauchst du in der Regel nicht: Auch ein File-Objekt kannst du mit file.toPath() an Files.newBufferedWriter() übergeben.

Wie schnell ist der PrintWriter?

Wie schnell der PrintWriter und die Klassen hinter ihm schreiben, habe ich mit dem Java Microbenchmark Harness (JMH) gemessen. Für jede Variante schreibt das Testprogramm WriteBenchmark dieselben 1.222.747 Zeilen mit 0 bis 160 Zeichen in eine Datei. Mit UTF-8 kodiert, sind das 100.000.000 Bytes – genau die Testdatei, die der BufferedReader-Artikel liest.

Das folgende Diagramm zeigt, wie lange jede Variante für die ganze Datei braucht, in Millisekunden. Blau sind die PrintWriter-Varianten, die alle mit println() schreiben, grau zum Vergleich ein BufferedWriter mit write() und newLine() und ein PrintStream um einen BufferedOutputStream:

Schreibzeiten für 1.222.747 Zeilen (100.000.000 Bytes): BufferedWriter mit write() und newLine() 122 ms, PrintWriter um Files.newBufferedWriter() 121 ms, PrintWriter(File, UTF_8) 119 ms, PrintWriter um FileWriter 439 ms, PrintWriter mit autoFlush 2.052 ms, PrintStream um BufferedOutputStream 141 ms
Um einen BufferedWriter schreibt der PrintWriter die Datei in 121 ms, um einen FileWriter in 439 ms

Ein PrintWriter um den BufferedWriter aus Files.newBufferedWriter() (Zeile 2 im Diagramm) braucht 121 ms, der BufferedWriter allein 122 ms (Zeile 1). Der PrintWriter selbst kostet also keine messbare Zeit. Mit dem Konstruktor mit File-Objekt (Zeile 3) sind es 119 ms, denn dieser Konstruktor baut dieselbe Kette.

Um einen FileWriter (Zeile 4) braucht der PrintWriter 439 ms und ist damit 263 % langsamer.

Wo die Zeit bleibt, zeigt ein zweites Programm, WriteCallCounter, das zählt, wie viele Aufrufe beim OutputStreamWriter ankommen und wie oft dieser in den OutputStream dahinter schreibt:

1222747 lines
PrintWriter -> OutputStreamWriter:
  2,445,494 calls to the OutputStreamWriter, 195,317 writes to the OutputStream
PrintWriter -> BufferedWriter -> OutputStreamWriter:
  12,088 calls to the OutputStreamWriter, 12,208 writes to the OutputStream
PrintWriter(autoFlush) -> BufferedWriter -> OutputStreamWriter:
  1,222,747 calls to the OutputStreamWriter, 1,222,747 writes to the OutputStream

Ohne BufferedWriter bekommt der OutputStreamWriter zwei Aufrufe pro Zeile, einen für den Text und einen für das Zeilenende. Und weil keiner dieser Aufrufe mehr als 160 Zeichen bringt, bleibt sein Byte-Puffer bei 512 Bytes: Er schreibt 195.317-mal in die Datei statt 12.208-mal.

Welcher der beiden Effekte die Zeit kostet, zeigt eine Gegenprobe: Legst du hinter den OutputStreamWriter einen BufferedOutputStream, der die Bytes wieder zu Blöcken von 8.192 Bytes sammelt, braucht der PrintWriter 136 ms. Das einzelne Kodieren der 2.445.494 Aufrufe kostet also 15 ms, die 183.109 zusätzlichen Schreibzugriffe auf die Datei kosten 303 ms – rund 1,7 µs pro Zugriff.

Mit autoFlush (Zeile 5 im Diagramm oben) braucht der PrintWriter 2.052 ms, 17-mal so lange wie ohne. Er leert den Puffer nach jeder Zeile und schreibt so 1.222.747-mal in die Datei. Pro zusätzlichem Schreibzugriff sind das 1,6 µs, etwa so viel wie beim FileWriter.

Der PrintStream (Zeile 6 im Diagramm) braucht 141 ms und ist damit 17 % langsamer als der PrintWriter. Er kodiert jeden println()-Aufruf einzeln und gibt die Bytes sofort an seinen BufferedOutputStream weiter, wie der Abschnitt PrintWriter vs. PrintStream zeigt.

printf() vs. println()

Ein zweiter Benchmark, FormatBenchmark, schreibt 1.000.000 Log-Zeilen wie die aus der Einleitung, den Log-Level auf fünf Zeichen aufgefüllt – mit printf(), mit println() und einem per + verketteten String, und mit einem print()-Aufruf pro Feld und Leerzeichen. Die letzte Variante misst er zweimal, einmal mit einem BufferedWriter hinter dem PrintWriter und einmal mit einem FileWriter.

Das folgende Diagramm zeigt, wie lange jede Variante für alle Zeilen braucht, in Millisekunden. Alle vier Balken sind PrintWriter-Varianten und deshalb blau:

Schreibzeiten für 1.000.000 Log-Zeilen: printf() 182 ms, println() mit verkettetem String 65 ms, print() pro Feld 77 ms, print() pro Feld um einen FileWriter 298 ms
printf() braucht für 1.000.000 Log-Zeilen 182 ms, println() mit verkettetem String 65 ms

printf() braucht fast dreimal so lange wie println(), denn der Formatter zerlegt den Format-String bei jedem Aufruf neu. Pro Zeile sind das allerdings nur rund 120 ns mehr: 182 ns statt 65 ns. Ich empfehle printf() überall, wo es den Code lesbarer macht, und println() mit + dort, wo du Millionen Zeilen schreibst und die Zeit zählt.

Setzt du jede Zeile mit einem print()-Aufruf pro Feld zusammen, braucht das 77 ms, 18 % länger als die verkettete Zeile. Um einen FileWriter braucht es 298 ms, fast viermal so lange. Der Grund ist derselbe wie in Zeile 4 des Diagramms oben: Ohne BufferedWriter landet jeder der sieben bis acht Aufrufe pro Zeile einzeln beim OutputStreamWriter, und der schreibt die kodierten Bytes in Blöcken von 512 Bytes in die Datei.

Häufige Fehler

Die folgenden Fehler zeigen die Abschnitte oben jeweils ausführlich:

  • Der PrintWriter wird nie geschlossen: Die Zeichen bleiben im Puffer, und die Datei ist nach dem Programmende leer.
  • Der PrintWriter umhüllt einen FileWriter direkt: Ohne BufferedWriter dazwischen schreibt er die Datei in Blöcken von 512 Bytes und braucht in der Messung 3,6-mal so lange.
  • autoFlush bei einer Datei, die niemand mitliest: Jede Zeile wird ein eigener Schreibzugriff, und das Schreiben dauert 17-mal so lange.
  • Niemand fragt checkError() ab: Ein Schreibfehler bleibt dann unbemerkt, und die Datei kann Teile doppelt enthalten.
  • write(int) statt print(int): write(65) schreibt ein „A“, keine „65“.
  • println() in einem Format mit festem Zeilenende: Auf Windows schreibt println() \r\n, auf Linux und macOS \n – je nach Betriebssystem.
  • printf() ohne Locale für eine Datei, die ein Programm liest: Mit deutschen Zahlenformaten steht dort ein Komma statt eines Punkts.
  • APPEND ohne CREATE: Beim ersten Lauf, wenn die Datei noch nicht existiert, wirft Files.newBufferedWriter() eine NoSuchFileException.
  • Ein Try-with-resources schließt einen PrintWriter um System.out: Danach erscheint keine Ausgabe über System.out mehr.

Zusammenfassung

Ein PrintWriter umhüllt einen Writer oder OutputStream und schreibt Werte als Text: mit print() und println() über String.valueOf(), mit printf() nach einem Format-String. Puffern und Kodieren überlässt er den Klassen hinter ihm.

Welche Klassen das sind, hängt vom PrintWriter-Konstruktor ab. Übergibst du einen Dateinamen, ein File-Objekt oder einen OutputStream, erzeugt der Konstruktor in allen drei Fällen dahinter einen BufferedWriter und einen OutputStreamWriter. Einen Writer umhüllt er ohne Puffer. Um einen FileWriter braucht der PrintWriter deshalb in der Messung 439 ms für eine Datei von 100 MB, um einen BufferedWriter 121 ms – so lange wie der BufferedWriter allein.

Ein PrintWriter wirft keine IOException. Fehler beim Schreiben zeigt nur checkError() an, Fehler im Format-String dagegen meldet printf() mit einer Exception. autoFlush leert den Puffer nach println(), printf() und format(), aber nicht nach print().

Meine Empfehlungen:

  • Erzeuge den PrintWriter für Dateien mit new PrintWriter(Files.newBufferedWriter(path)). Er kodiert ohne weitere Angabe als UTF-8, und ein Zeichen, das nicht in eine andere gewählte Kodierung passt, meldet er über checkError(), statt es zu ersetzen.
  • Gib zum Anhängen CREATE und APPEND an.
  • Schließe den PrintWriter mit Try-with-resources, außer er umhüllt System.out.
  • Ruf checkError() als letzte Anweisung im Try-Block auf, wenn du dich auf die Datei verlassen musst.
  • Schalte autoFlush nur ein, wo jemand mitliest.
  • Gib bei printf() für maschinenlesbare Dateien Locale.ROOT an.

Wie du Strings, Listen und Byte-Arrays mit einem einzigen Methodenaufruf in eine Datei schreibst, zeigt der Artikel über das Schreiben von Dateien aus derselben Serie. Wie du die geschriebene Datei Zeile für Zeile wieder einliest, zeigt der Artikel über den BufferedReader.

Wenn dir dieser Artikel gefallen hat, freue ich mich über eine Bewertung auf meinem ProvenExpert-Profil.

👉 Bewertung abgeben

Lust auf noch mehr Wissen?

In meinem Blog findest du viele Artikel zu Java, Softwarearchitektur und Performance von grundlegenden Konzepten bis hin zu fortgeschrittenen Patterns.

Wenn du tiefer einsteigen willst, schau dir meine Trainings an: praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Zu den Java-Schulungen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche