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 einBufferedWriterverwendet undwritemanuell 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/formatprint 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 \nWenn 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.
Was aus dem Durchlauf mitgenommen werden sollte:
- Die CSV-Datei entstand genau so, wie die
printf-Formatierung es vorgab.%.2frundete den Preis auf zwei Nachkommastellen;%-10srichtete den Text linksbündig in einer 10-Zeichen-Spalte aus;\n(nicht%n) hielt den Zeilenabschluss portabel. Format-Strings sind der wichtigste Grund,PrintWritergegenüber einem einfachenWriterzu bevorzugen. - Das erste
try-with-resources fasste den vierlagigen Stack —Path→FileOutputStream→OutputStreamWriter(UTF-8)→BufferedWriter→PrintWriter— 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. Sobaldclose()ausgeführt wird, ist es schwieriger, auf den Flag-Status zu reagieren — dertry-Block wurde bereits verlassen. Innerhalb des Blocks ist der richtige Zeitpunkt zum Prüfen. - Der autoflush-aktivierte
PrintWriter, derSystem.outumschließt, druckte den Bericht spaltenausgerichtet, weil jedesprintfmit%nendete, was den Flush auslöste. Er ist nicht in einemtry-with-resources eingeschlossen — einPrintWriterzu schließen, derSystem.outdekoriert, würdeSystem.outselbst schließen und alles danach Gedruckte zum Schweigen bringen, weshalb das Beispiel stattdessen flush aufruft. - Der vierte Block konstruierte einen Writer, dessen
writeimmer eine Ausnahme wirft, und richtete einenPrintWriterdarauf aus. Dasprintlnkehrte normal zurück — keine Ausnahme zum Abfangen.checkError()gabtruezurü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.