Java PrintStream
Come PrintStream alimenta System.out e System.err e come usarlo per output formattato orientato ai byte.
PrintStream è la classe che opera sotto il tuo codice fin dal capitolo 1. System.out è un PrintStream. System.err è un PrintStream. Ogni System.out.println(...) che hai mai scritto è passato attraverso questa classe.
Ha la stessa interfaccia del PrintWriter che hai appena incontrato — print, println, printf, format — e lo stesso comportamento di assorbimento delle eccezioni. La differenza sta in ciò su cui si basa: PrintStream estende OutputStream (byte), mentre PrintWriter estende Writer (caratteri). Per l'output su file, la distinzione byte/carattere vista in precedenza in questa parte si applica ancora: caratteri in ingresso, caratteri in uscita, e la codifica risiede al confine.
Perché due classi con la stessa API?
È storia. Java 1.0 aveva PrintStream ma nessuna gerarchia Writer — ogni "print" andava a un byte stream. Java 1.1 ha introdotto la gerarchia Reader/Writer per una corretta gestione dei caratteri e ha aggiunto PrintWriter in modo che il codice di scrittura su file potesse usare la stessa API sui caratteri. PrintStream non poteva essere rimosso perché System.out e System.err erano già tipizzati come PrintStream nelle API pubblicate, e cambiarli avrebbe rotto ogni programma nel mondo.
Quindi esistono entrambi. La regola pratica:
- Usa
PrintWriterper i file. La gerarchia orientata ai caratteri è dove appartiene la codifica. - Usa
PrintStreamquando devi — cioè, quandoSystem.out/System.errè la destinazione, o quando stai scrivendo su unOutputStreamche non vuoi wrappare.
I casi di "devi" sono limitati. La maggior parte delle volte puoi fare così:
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8);
out.println("hello");e dimenticarti che PrintStream esiste.
L'API
Identica a 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);Più i metodi OutputStream ereditati (write(int), write(byte[]), flush, close). La stessa trappola di BufferedWriter e PrintWriter si applica: println scrive System.lineSeparator(), che è \r\n su Windows. Scrivi \n esplicitamente quando l'output deve essere portabile.
Costruttori
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);Come con PrintWriter, i costruttori senza charset ricadono sulla codifica predefinita della JVM — lo stesso rischio di portabilità descritto nel capitolo sugli stream di caratteri. Passa sempre un charset.
Il flag autoFlush ha la stessa semantica di PrintWriter: quando attivo, println, printf, format e write(byte[], int, int) su un newline attivano un flush. print non lo fa. Disattivo per impostazione predefinita.
La IOException inghiottita (ancora)
Stesso design di PrintWriter. Nessuno dei metodi print/println/printf lancia IOException. Una scrittura fallita imposta un flag di errore che si legge con checkError(). Il compromesso è lo stesso: comodo per codice occasionale, pericoloso se non verifichi.
Per System.out/System.err specificamente, inghiottire è la scelta giusta — non c'è nulla di utile da fare quando una scrittura sul terminale fallisce. Per un PrintStream su file, preferisci PrintWriter, o controlla checkError() prima di chiudere.
System.out e System.err
Questi due sono istanze di PrintStream create durante l'avvio della JVM. Wrappano i descrittori di file stdout e stderr del sistema operativo. La loro codifica dei caratteri segue stdout.encoding (Java 18+) o file.encoding (versioni più vecchie), motivo per cui l'output reindirizzato tramite pipe a volte produce mojibake su una console Windows — la code page della console non corrisponde all'idea di codifica della JVM.
Puoi sostituirli con System.setOut(PrintStream) e System.setErr(PrintStream), il che è occasionalmente utile per catturare l'output nei test:
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);
}Per il codice di produzione, lasciali stare. I framework di logging (java.util.logging, SLF4J/Logback) adottano un approccio diverso e strutturato per scrivere output diagnostico.
print(Object) e null
Un comportamento sottile condiviso con PrintWriter: print(Object o) chiama String.valueOf(o), che restituisce la stringa di quattro caratteri "null" per un riferimento null invece di lanciare NullPointerException. Questo è il motivo per cui
System.out.println(maybeNullList); // prints "null", not NPEfunziona. Comodo per il logging occasionale; fuorviante se stai scrivendo la stringa in un file di dati che rileggerai in seguito — "null" come stringa è indistinguibile dalla parola letterale "null."
write(int) scrive un byte, non un carattere
PrintStream è un OutputStream. Il metodo ereditato write(int b) scrive il byte di ordine basso:
System.out.write(65); // writes 'A' — the byte 0x41
System.out.write('é'); // writes a single byte 0xE9 — NOT UTF-8 for 'é'La seconda riga è sbagliata su un terminale UTF-8 — 'é' è due byte in UTF-8 (0xC3 0xA9), e ne hai scritto uno solo. Non usare write(int) su un PrintStream per i caratteri; usa print/println, che passano attraverso il charset configurato.
Un esempio concreto: System.out reindirizzato e ispezionato
Il programma seguente cattura System.out in un ByteArrayOutputStream per vedere esattamente quali byte emette la JVM quando chiami println. Esegue la stessa println("Café") con due charset diversi per rendere concreto il comportamento della codifica, dimostra checkError() su uno stream fallimentare, e infine mostra la differenza tra print(Object) per un riferimento null e un controllo null esplicito.
Cosa trarre dall'esecuzione:
System.setOut(new PrintStream(buffer, ...))ha catturato ciò che altrimenti sarebbe andato alla console. I test usano questo pattern continuamente. Ripristina l'originale prima di stampare il report — altrimenti il report va nel buffer, e ne deriva confusione.- La riga "Café" ha emesso 5 byte in UTF-8 (
43 61 66 C3 A9) e 4 byte in ISO-8859-1 (43 61 66 E9). Stesso input, larghezze in byte diverse, entrambi corretti — la codifica è la mappatura byte → carattere, ePrintStreamrispetta il charset fornito al suo costruttore. Il costruttore senza charset avrebbe scelto quello in uso dalla JVM in quel momento. - Il blocco con lo stream rotto ha dimostrato l'inghiottimento:
printlnè tornato normalmente, laIOExceptionsottostante è scomparsa, echeckError()era l'unico modo per scoprire che la scrittura era fallita. Stesso contratto diPrintWriter. Se ti interessa il fallimento, devi chiedere. - La stampa del riferimento null ha prodotto la stringa di quattro caratteri
null, non unaNullPointerException. È così cheprintln(someList)funziona anche quandosomeListènull— comodo, ma significa che non puoi distinguere il testo letterale "null" da un riferimento null una volta che è su disco. UsaObjects.requireNonNullo un controllo null esplicito al confine se quella distinzione è importante. - Nulla nell'esempio ha chiamato un
PrintWriter. PerSystem.out, non ne hai bisogno —PrintStreamè il tipo che Java ti ha già dato, l'API è identica, e il comportamento di autoflush suprintlnè quello che vuoi al terminale.
Cosa viene dopo
I primi tredici capitoli di questa parte hanno coperto ogni forma di I/O in streaming: byte, caratteri, buffering, primitivi, testo formattato. Trasmettono tutti contenuto — byte e char. Il prossimo capitolo, Java Serialization, riguarda la trasmissione di grafi di oggetti — un'intera struttura collegata di riferimenti, scritta su uno stream e ricostruita dall'altro lato, con una singola annotazione sulla classe.