Java PrintStream
Wie PrintStream System.out und System.err antreibt und wie man ihn für byteorientierte formatierte Ausgabe verwendet.
PrintStream ist die Klasse, die seit Kapitel 1 hinter Ihrem Code steckt. System.out ist ein PrintStream. System.err ist ein PrintStream. Jedes System.out.println(...), das Sie je geschrieben haben, ist durch diese Klasse gegangen.
Sie hat dieselbe Oberfläche wie der PrintWriter, den Sie gerade kennengelernt haben — print, println, printf, format — und dasselbe Verhalten, Ausnahmen zu verschlucken. Der Unterschied liegt in dem, worauf sie aufsetzt: PrintStream erweitert OutputStream (Bytes), während PrintWriter Writer (Zeichen) erweitert. Für die Dateiausgabe gilt die Unterscheidung Byte/Zeichen aus dem früheren Teil dieses Abschnitts weiterhin: Zeichen rein, Zeichen raus, und das Encoding liegt an der Grenze.
Warum zwei Klassen mit derselben API?
Geschichte. Java 1.0 hatte PrintStream, aber überhaupt keine Writer-Hierarchie — jede "print"-Operation ging an einen Byte-Stream. Java 1.1 führte die Reader/Writer-Hierarchie für eine korrekte Zeichenbehandlung ein und fügte PrintWriter hinzu, damit Datei-Schreibcode dieselbe API auf Zeichen verwenden konnte. PrintStream konnte nicht zurückgezogen werden, weil System.out und System.err bereits als PrintStream in veröffentlichten APIs typisiert waren, und eine Änderung hätte jedes Programm der Welt gebrochen.
Daher existieren beide. Die praktische Regel:
- Verwenden Sie
PrintWriterfür Dateien. Die zeichenorientierte Hierarchie ist dort, wo das Encoding hingehört. - Verwenden Sie
PrintStream, wenn Sie müssen — d.h. wennSystem.out/System.errdas Ziel ist oder wenn Sie in einenOutputStreamschreiben, den Sie nicht umbrechen möchten.
Die "Muss"-Fälle sind selten. Meistens können Sie Folgendes tun:
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8);
out.println("hello");und vergessen, dass PrintStream existiert.
Die API
Identisch mit PrintWriter:
void print(boolean | char | int | long | float | double | String | Object);
void println(...); // adds the platform line separator
PrintStream printf(String format, Object... args);
PrintStream format(String format, Object... args);
PrintStream append(CharSequence s);Dazu die geerbten OutputStream-Methoden (write(int), write(byte[]), flush, close). Die gleiche Falle wie bei BufferedWriter und PrintWriter gilt: println schreibt System.lineSeparator(), was auf Windows \r\n ist. Schreiben Sie \n explizit, wenn die Ausgabe portabel sein muss.
Konstruktoren
new PrintStream(OutputStream out); // platform default charset
new PrintStream(OutputStream out, boolean autoFlush, Charset cs); // explicit charset
new PrintStream(File file, Charset charset); // open a file
new PrintStream(String filename, Charset charset);Wie bei PrintWriter fallen die Konstruktoren ohne Charset auf das JVM-Standard-Encoding zurück — dasselbe Portabilitätsproblem, das im Kapitel über Zeichenströme beschrieben wird. Geben Sie immer einen Charset an.
Das autoFlush-Flag hat dieselbe Semantik wie bei PrintWriter: Wenn es aktiviert ist, lösen println, printf, format und write(byte[], int, int) bei einem Zeilenumbruch einen Flush aus. print tut dies nicht. Standardmäßig deaktiviert.
Die verschluckte IOException (weiterhin)
Gleiches Design wie PrintWriter. Keine der print/println/printf-Methoden wirft IOException. Ein fehlgeschlagener Schreibvorgang setzt ein Fehler-Flag, das Sie mit checkError() lesen. Der Kompromiss ist derselbe: praktisch für gelegentlichen Code, gefährlich, wenn Sie nicht prüfen.
Für System.out/System.err speziell ist das Verschlucken die richtige Entscheidung — es gibt nichts Sinnvolles zu tun, wenn ein Schreibvorgang auf dem Terminal fehlschlägt. Für einen dateigestützten PrintStream bevorzugen Sie PrintWriter oder prüfen Sie checkError() vor dem Schließen.
System.out und System.err
Diese beiden sind PrintStream-Instanzen, die beim JVM-Start erstellt werden. Sie umhüllen die stdout- und stderr-Dateideskriptoren des Betriebssystems. Ihre Zeichenkodierung folgt stdout.encoding (Java 18+) oder file.encoding (ältere Versionen), weshalb pipe-umgeleitete Ausgabe auf einer Windows-Konsole manchmal Mojibake erzeugt — die Codepage der Konsole stimmt nicht mit der JVM-Vorstellung des Encodings überein.
Sie können sie mit System.setOut(PrintStream) und System.setErr(PrintStream) ersetzen, was gelegentlich nützlich ist, um die Ausgabe in Tests abzufangen:
ByteArrayOutputStream captured = new ByteArrayOutputStream();
PrintStream original = System.out;
System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8));
try {
runTheCodeUnderTest();
assertEquals("expected\n", captured.toString(StandardCharsets.UTF_8));
} finally {
System.setOut(original);
}Für Produktionscode lassen Sie sie in Ruhe. Logging-Frameworks (java.util.logging, SLF4J/Logback) verfolgen einen anderen, strukturierten Ansatz zum Schreiben von Diagnoseausgaben.
print(Object) und null
Ein subtiles Verhalten, das mit PrintWriter geteilt wird: print(Object o) ruft String.valueOf(o) auf, was für eine null-Referenz den vierzeichigen String "null" zurückgibt, anstatt eine NullPointerException zu werfen. Das ist der Grund, warum
System.out.println(maybeNullList); // prints "null", not NPEfunktioniert. Praktisch für gelegentliches Logging; irreführend, wenn Sie den String zurück in eine Datendatei schreiben, die Sie später parsen werden — "null" als String ist vom wörtlichen Wort "null" nicht zu unterscheiden.
write(int) schreibt ein Byte, kein Zeichen
PrintStream ist ein OutputStream. Das geerbte write(int b) schreibt das niedrigwertige Byte:
System.out.write(65); // writes 'A' — the byte 0x41
System.out.write('é'); // writes a single byte 0xE9 — NOT UTF-8 for 'é'Die zweite Zeile ist auf einem UTF-8-Terminal falsch — 'é' besteht in UTF-8 aus zwei Bytes (0xC3 0xA9), und Sie haben nur eins geschrieben. Verwenden Sie write(int) nicht für Zeichen auf einem PrintStream; verwenden Sie print/println, die durch den konfigurierten Charset gehen.
Ein durchgearbeitetes Beispiel: System.out umgeleitet und untersucht
Das folgende Programm fängt System.out in einem ByteArrayOutputStream ab, sodass Sie genau sehen können, welche Bytes die JVM ausgibt, wenn Sie println aufrufen. Es führt dasselbe println("Café") mit zwei verschiedenen Charsets aus, um das Encoding-Verhalten konkret zu machen, demonstriert checkError() auf einem fehlerhaften Stream und zeigt schließlich den Unterschied zwischen print(Object) für eine null-Referenz und einer expliziten Null-Prüfung.
Was aus dem Lauf zu entnehmen ist:
System.setOut(new PrintStream(buffer, ...))hat abgefangen, was sonst auf der Konsole ausgegeben worden wäre. Tests verwenden dieses Muster ständig. Stellen Sie das Original wieder her, bevor Sie Ihren Bericht ausgeben — andernfalls geht der Bericht ebenfalls in den Puffer, und Verwirrung folgt.- Die "Café"-Zeile hat in UTF-8 5 Bytes ausgesendet (
43 61 66 C3 A9) und in ISO-8859-1 4 Bytes (43 61 66 E9). Gleiche Eingabe, unterschiedliche Byte-Breiten, beide korrekt — Encoding ist die Byte-Zeichen-Zuordnung, undPrintStreamberücksichtigt den Charset, den Sie seinem Konstruktor übergeben haben. Der Konstruktor ohne Charset würde denjenigen wählen, mit dem die JVM gerade läuft. - Der Block mit dem fehlerhaften Stream hat das Verschlucken bewiesen:
printlnkehrte normal zurück, die zugrunde liegendeIOExceptionverschwand, undcheckError()war der einzige Weg, herauszufinden, dass der Schreibvorgang fehlgeschlagen war. Derselbe Vertrag wie beiPrintWriter. Wenn Sie sich um den Fehler kümmern, müssen Sie fragen. - Der Null-Referenz-Ausdruck erzeugte den vierzeichigen String
null, keineNullPointerException. So funktioniertprintln(someList)auch wennsomeListnullist — praktisch, aber es bedeutet, dass Sie den wörtlichen Text "null" nicht mehr von einer Null-Referenz unterscheiden können, sobald er auf der Festplatte ist. Wählen SieObjects.requireNonNulloder eine explizite Null-Prüfung an der Grenze, wenn diese Unterscheidung wichtig ist. - Nichts im Beispiel hat einen
PrintWriteraufgerufen. FürSystem.outbrauchen Sie keinen —PrintStreamist der Typ, den Java Ihnen bereits gegeben hat, die API ist identisch, und das autoflush-bei-println-Verhalten ist das, was Sie am Terminal wollen.
Was kommt als Nächstes
Die ersten dreizehn Kapitel dieses Teils haben jede Form von Streaming-I/O abgedeckt: Bytes, Zeichen, Pufferung, Primitive, formatierten Text. Sie alle übertragen Inhalte — Bytes und Zeichen. Das nächste Kapitel, Java Serialization, dreht sich um die Übertragung von Objektgraphen — eine ganze verknüpfte Referenzstruktur, die in einen Stream geschrieben und auf der anderen Seite rekonstruiert wird, mit einer einzigen Annotation an der Klasse.