W3docs

Java PrintWriter

Formatierten Text in Java mit der PrintWriter-Klasse schreiben — print, println, printf, format erklärt.

Das Kapitel über Zeichenströme hat Writer und seine Kernmethode write(String) eingeführt. Das reicht für alles aus, ist aber nicht ergonomisch — eine Zahl zu drucken bedeutet w.write(Integer.toString(n)), eine Zeile zu drucken bedeutet, den Zeilenabschluss selbst anzuhängen, und für formatierten Output muss bei jedem Aufruf String.format verwendet werden. PrintWriter ist der Decorator, der die Ergonomik behebt: Er fügt print, println, printf und format auf Basis eines beliebigen Writer- oder OutputStream-Objekts hinzu.

Er ist das dateibasierte Gegenstück zu System.out, das seit dem ersten Kapitel verwendet wird — gleiche API-Oberfläche, schreibt aber in eine Datei statt auf die Konsole.

Was PrintWriter hinzufügt

void  print(boolean | char | int | long | float | double | String | Object);
void  println(...);                                  // same overloads, plus the line terminator
PrintWriter printf(String format, Object... args);   // String.format under the hood
PrintWriter format(String format, Object... args);   // alias for printf
PrintWriter append(CharSequence s);                  // returns this (for chaining)

Dazu kommen die geerbten Writer-Methoden (write, flush, close). Der Vorteil sind die typisierten Überladungen: Jeder primitive Wert oder jedes Objekt kann direkt geschrieben werden, und PrintWriter ruft String.valueOf automatisch auf.

try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(path))) {
  w.println("header");
  w.printf("count = %d%n", 42);
  w.printf("rate  = %.2f%%%n", 0.875 * 100);
  w.println();                                       // blank line
}

Das try-with-resources-close() leert den Puffer; ohne es gilt die Tail-Buffer-Falle aus dem Kapitel zu gepufferten Strömen genauso für PrintWriter.

Konstruktoren

Die nützlichen Konstruktoren in der Reihenfolge ihrer Bevorzugung:

PrintWriter(Path file, Charset charset);             // Java 10+, opens the file with the charset
PrintWriter(Writer out);                             // wrap any Writer (typical: a BufferedWriter)
PrintWriter(Writer out, boolean autoFlush);          // same, with autoFlush on println/printf
PrintWriter(OutputStream out, boolean autoFlush, Charset charset);

Der Path- + Charset-Konstruktor ist der einfachste für „Datei öffnen und hineinschreiben":

try (PrintWriter w = new PrintWriter(path.toFile(), StandardCharsets.UTF_8)) {
  w.println("hello");
}

Er öffnet die Datei, kapselt sie in einem OutputStreamWriter mit dem angegebenen Zeichensatz, kapselt das in einem BufferedWriter und liefert einen PrintWriter. Der vierlagige Stack, der früher manuell aufgebaut werden musste, kollabiert zu einer Zeile.

Immer einen expliziten Zeichensatz angeben. Die Konstruktoren ohne Zeichensatz — new PrintWriter("file.txt"), new PrintWriter(outputStream) — fallen auf die Standard-Kodierung der JVM zurück, was dasselbe Portabilitätsproblem ist wie im Kapitel zu Zeichenströmen beschrieben. UTF-8 ist der richtige Standard.

Die verschluckte IOException

PrintWriter unterscheidet sich von jedem anderen Writer in einem wichtigen Punkt: Er wirft keine IOException. Weder print, println, printf noch write deklarieren sie. Wenn ein zugrunde liegender I/O-Aufruf fehlschlägt, verschluckt PrintWriter die Ausnahme und setzt ein internes „Fehler"-Flag.

Das ist praktisch — ein langer Block mit println-Aufrufen kann ohne try/catch um jeden einzelnen geschrieben werden —, bedeutet aber, dass ein fehlgeschlagener Schreibvorgang lautlos bleibt. Man muss nachfragen:

if (w.checkError()) {
  // something went wrong; the underlying IOException was swallowed
  throw new IOException("write to " + path + " failed");
}

checkError() ist der einzige Weg, es herauszufinden. Es gibt keine Möglichkeit, die ursprüngliche IOException abzurufen — zum Zeitpunkt der Überprüfung ist sie bereits weg. Daher gilt:

  • Bei Konsolenausgabe (dem System.out-Anwendungsfall) ist das Verschlucken in Ordnung: Niemand behandelt einen fehlgeschlagenen Schreibvorgang in ein Terminal.
  • Bei Dateien, bei denen ein teilweiser Schreibvorgang ein echtes Problem ist (eine Logdatei, eine Speicherdatei, ein generierter Bericht), sollte checkError() am Ende des Blocks aufgerufen werden — oder ein BufferedWriter verwendet und write manuell aufgerufen werden, damit die Ausnahme weitergegeben wird.

Autoflush

Das Konstruktorargument autoFlush steuert, ob jeder Aufruf von println, printf oder format anschließend flush() aufruft. Standard ist aus:

new PrintWriter(out, false)                          // explicit close/flush only
new PrintWriter(out, true)                           // flush after every println/printf/format

print und write führen nie einen Autoflush durch, auch wenn das Flag gesetzt ist — nur die Zeilen- und Formatierungsmethoden tun es. Deshalb kann System.out.print("waiting...") unsichtbar bleiben, während die nächste Berechnung läuft, während System.out.println("waiting...") sofort angezeigt wird.

Bei Dateien sollte Autoflush deaktiviert bleiben und close() (via try-with-resources) das Leeren übernehmen. Bei einem interaktiven Log, der mitverfolgt wird, kann er eingeschaltet oder nach jedem Batch flush() aufgerufen werden.

println und der plattformabhängige Zeilenabschluss

println() schreibt System.lineSeparator()\n unter Unix und macOS, \r\n unter Windows. Gleiches gilt für den %n-Bezeichner in printf. Das ist eine Funktion für die Terminalausgabe und ein Fehler für Datendateien; die Diskussion im Kapitel zu gepufferten Strömen (unter BufferedWriter.newLine) gilt hier genauso:

w.printf("row,%d%n", 1);                             // platform-dependent terminator
w.printf("row,%d\n", 1);                             // portable \n

Wenn die Ausgabe von einer anderen Maschine als der lokalen gelesen werden soll, sollte \n explizit geschrieben werden.

Vergleich mit PrintStream

PrintStream ist das byteorientierte Geschwisterstück von PrintWriter — gleiche API, anderer Vorfahre. System.out und System.err sind PrintStream-Instanzen. Für die Dateiausgabe ist PrintWriter vorzuziehen: Es zwingt dazu, über die Zeichenkodierung nachzudenken (da der Konstruktor einen Charset erwartet), während PrintStream für jedes nicht-ASCII-Zeichen stillschweigend die Standard-Kodierung verwendet.

Das nächste Kapitel, Java PrintStream, geht ausführlich auf die Unterschiede ein.

Ein praktisches Beispiel: Eine kleine CSV-Datei schreiben

Das folgende Programm öffnet eine temporäre Datei mit einem PrintWriter, schreibt eine kleine CSV-Datei (Kopfzeile + Zeilen), demonstriert die printf-Formatierung, ruft checkError() auf, um die erfolgreichen Schreibvorgänge zu bestätigen, und zeigt abschließend das Verhalten bei verschluckten Ausnahmen, indem es in einen Writer schreibt, dessen zugrunde liegender Stream bereits geschlossen wurde.

java— editable, runs on the server

Was aus dem Durchlauf mitgenommen werden sollte:

  • Die CSV-Datei entstand genau so, wie die printf-Formatierung es vorgab. %.2f rundete den Preis auf zwei Nachkommastellen; %-10s richtete den Text linksbündig in einer 10-Zeichen-Spalte aus; \n (nicht %n) hielt den Zeilenabschluss portabel. Format-Strings sind der wichtigste Grund, PrintWriter gegenüber einem einfachen Writer zu bevorzugen.
  • Das erste try-with-resources fasste den vierlagigen Stack — PathFileOutputStreamOutputStreamWriter(UTF-8)BufferedWriterPrintWriter — zu einem einzigen Konstruktoraufruf zusammen. Der (File, Charset)-Konstruktor ist der richtige für „Datei öffnen, Text hineinschreiben, sauber schließen."
  • Die checkError()-Prüfung lief vor dem Schließen. Sobald close() ausgeführt wird, ist es schwieriger, auf den Flag-Status zu reagieren — der try-Block wurde bereits verlassen. Innerhalb des Blocks ist der richtige Zeitpunkt zum Prüfen.
  • Der autoflush-aktivierte PrintWriter, der System.out umschließt, druckte den Bericht spaltenausgerichtet, weil jedes printf mit %n endete, was den Flush auslöste. Er ist nicht in einem try-with-resources eingeschlossen — ein PrintWriter zu schließen, der System.out dekoriert, würde System.out selbst schließen und alles danach Gedruckte zum Schweigen bringen, weshalb das Beispiel stattdessen flush aufruft.
  • Der vierte Block konstruierte einen Writer, dessen write immer eine Ausnahme wirft, und richtete einen PrintWriter darauf aus. Das println kehrte normal zurück — keine Ausnahme zum Abfangen. checkError() gab true zurück, was der einzige Weg war, herauszufinden, dass der Schreibvorgang fehlgeschlagen war. Das ist der Kompromiss des Verschlucken-und-Flag-Designs: praktisch für Gelegenheitscode, gefährlich, wenn nicht geprüft wird.

Was kommt als Nächstes

PrintWriter ist der zeichenorientierte Dateischreiber. Sein Geschwisterstück, Java PrintStream, ist das byteorientierte, das System.out und System.err antreibt — gleiche API, anderer Vorfahre, und der Grund, warum die Terminalausgabe für jedes Zeichen funktioniert, das in den Standard-Zeichensatz der Plattform passt.

Übungen

Übung
Du schreibst eine 10.000-zeilige Datei mit `PrintWriter` und rufst nie `checkError()` auf. Drei Zeilen in der Mitte konnten wegen einer vollen Festplatte nicht geschrieben werden. Wie sieht die resultierende Datei aus, und was meldet das Programm?
Du schreibst eine 10.000-zeilige Datei mit `PrintWriter` und rufst nie `checkError()` auf. Drei Zeilen in der Mitte konnten wegen einer vollen Festplatte nicht geschrieben werden. Wie sieht die resultierende Datei aus, und was meldet das Programm?
Was this page helpful?