Java PrintWriter
Scrivi testo formattato su stream in Java con la classe PrintWriter — print, println, printf, format.
Il capitolo sugli stream di caratteri ha introdotto Writer e il suo metodo principale, write(String). È sufficiente per qualsiasi cosa, ma non è ergonomico — stampare un numero significa w.write(Integer.toString(n)), stampare una riga significa ricordarsi di aggiungere manualmente il terminatore di riga, e l'output formattato richiede String.format a ogni chiamata. PrintWriter è il decoratore che risolve il problema dell'ergonomia: aggiunge print, println, printf e format sopra qualsiasi Writer o OutputStream sottostante.
È la controparte lato file di System.out che hai usato fin dal primo capitolo — stessa superficie API, ma scritta su un file invece che sulla console.
Cosa aggiunge PrintWriter
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)Più i metodi Writer ereditati (write, flush, close). Il punto è la presenza di overload tipizzati: puoi scrivere qualsiasi primitiva o oggetto direttamente e PrintWriter chiama String.valueOf al posto tuo.
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
}Il close() del try-with-resources svuota il buffer; senza di esso, la trappola del buffer di coda descritta nel capitolo sugli stream bufferizzati si applica altrettanto a PrintWriter.
Costruttori
I più utili, in ordine di preferenza:
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);Il costruttore Path + Charset è il più semplice per "apri questo file e scrivi su di esso":
try (PrintWriter w = new PrintWriter(path.toFile(), StandardCharsets.UTF_8)) {
w.println("hello");
}Apre il file, lo avvolge in un OutputStreamWriter con il charset indicato, avvolge quest'ultimo in un BufferedWriter e ti restituisce un PrintWriter. Lo stack a quattro livelli che un tempo dovevi assemblare manualmente si riduce a una sola riga.
Passa sempre un charset esplicito. I costruttori senza charset — new PrintWriter("file.txt"), new PrintWriter(outputStream) — usano la codifica predefinita della JVM, che rappresenta lo stesso rischio di portabilità descritto nel capitolo sugli stream di caratteri. UTF-8 è il valore predefinito corretto.
L'IOException inghiottita
PrintWriter differisce da tutti gli altri Writer per un aspetto importante: non lancia IOException. Nessuno tra print, println, printf o write la dichiara. Se una chiamata I/O sottostante fallisce, PrintWriter inghiotte l'eccezione e imposta un flag interno di "errore".
Questo è comodo — puoi scrivere un lungo blocco di chiamate println senza try/catch attorno a ciascuna — ma significa che una scrittura fallita è silenziosa. Devi verificare esplicitamente:
if (w.checkError()) {
// something went wrong; the underlying IOException was swallowed
throw new IOException("write to " + path + " failed");
}checkError() è l'unico modo per scoprirlo. Non esiste alcun modo per recuperare l'IOException originale — quando la controlli, è già scomparsa. Quindi:
- Per l'output su console (il caso d'uso di
System.out), inghiottire l'eccezione va bene: nessuno gestisce una scrittura fallita su un terminale. - Per i file in cui una scrittura parziale è un problema reale (un file di log, un file di salvataggio, un report generato), controlla
checkError()alla fine del blocco, oppure usa unBufferedWritere chiamawritedirettamente così l'eccezione si propaga.
Autoflush
L'argomento del costruttore autoFlush controlla se ogni chiamata a println, printf o format invoca flush() a conclusione. Il valore predefinito è disattivato:
new PrintWriter(out, false) // explicit close/flush only
new PrintWriter(out, true) // flush after every println/printf/formatprint e write non eseguono mai l'autoflush, nemmeno con il flag attivo — solo i metodi di riga e formato lo fanno. Ecco perché System.out.print("waiting...") può rimanere invisibile mentre il calcolo successivo è in esecuzione, mentre System.out.println("waiting...") appare immediatamente.
Per i file, lascia l'autoflush disattivato e lascia che sia close() (tramite try-with-resources) a gestirlo. Per un log interattivo che stai monitorando in tempo reale, attivalo oppure chiama flush() dopo ogni batch.
println e il separatore di riga della piattaforma
println() scrive System.lineSeparator() — \n su Unix e macOS, \r\n su Windows. Lo stesso vale per l'identificatore %n in printf. Questo è un vantaggio per l'output su terminale e un problema per i file di dati; la discussione nel capitolo sugli stream bufferizzati (sotto BufferedWriter.newLine) si applica qui verbatim:
w.printf("row,%d%n", 1); // platform-dependent terminator
w.printf("row,%d\n", 1); // portable \nQuando l'output verrà letto da qualcosa che non sia il computer locale, scrivi \n esplicitamente.
Confronto con PrintStream
PrintStream è il fratello orientato ai byte di PrintWriter — stessa API, antenato diverso. System.out e System.err sono istanze di PrintStream. Per l'output su file, preferisci PrintWriter: ti costringe a ragionare sulla codifica dei caratteri (poiché il costruttore accetta un Charset), mentre PrintStream utilizzerà silenziosamente la codifica predefinita per qualsiasi carattere non ASCII che stampi.
Il capitolo successivo, Java PrintStream, approfondisce le differenze in dettaglio.
Un esempio pratico: scrivere un piccolo file CSV
Il programma seguente apre un file temporaneo con un PrintWriter, scrive un piccolo CSV (intestazione + righe), mostra la formattazione con printf, chiama checkError() per verificare che le scritture siano andate a buon fine, e infine illustra il comportamento di inghiottimento dell'eccezione scrivendo su uno writer il cui stream sottostante è stato chiuso.
Cosa osservare dall'esecuzione:
- Il CSV è uscito esattamente come specificato dal formato
printf.%.2fha arrotondato il prezzo a due decimali;%-10sha allineato a sinistra entro una colonna di 10 caratteri;\n(non%n) ha mantenuto il terminatore di riga portabile. Le stringhe di formato sono il motivo principale per sceglierePrintWriterrispetto a un sempliceWriter. - Il primo try-with-resources ha preso lo stack a quattro livelli —
Path→FileOutputStream→OutputStreamWriter(UTF-8)→BufferedWriter→PrintWriter— e lo ha collassato in una sola chiamata al costruttore. Il costruttore(File, Charset)è quello che vuoi per "apri questo file, scrivi testo su di esso, chiudilo in modo pulito." - Il controllo
checkError()è stato eseguito prima della chiusura. Una volta eseguitoclose(), verificare il flag è più difficile da tradurre in azioni — hai già lasciato il bloccotry. All'interno del blocco è il posto giusto per controllare. - Il
PrintWritercon autoflush attivo che avvolgeSystem.outha stampato il report con colonne allineate perché ogniprintfterminava con%n, il che ha scatenato il flush. Non è avvolto in un try-with-resources — chiudere unPrintWriterche decoraSystem.outchiuderebbeSystem.outstesso e silenzerebbe tutto ciò che viene stampato in seguito, quindi l'esempio usa invece flush. - Il quarto blocco ha costruito uno writer il cui
writelancia sempre un'eccezione e ha puntato unPrintWritersu di esso. Laprintlnè tornata normalmente — nessuna eccezione da catturare.checkError()ha restituitotrue, che era l'unico modo per scoprire che la scrittura era fallita. Questo è il compromesso del design swallow-and-flag: comodo per il codice informale, pericoloso se non si controlla.
Cosa viene dopo
PrintWriter è il file writer orientato ai caratteri. Il suo fratello, Java PrintStream, è quello orientato ai byte che alimenta System.out e System.err — stessa API, antenato diverso, e la ragione per cui l'output su terminale funziona per qualsiasi carattere che rientra nel charset predefinito della piattaforma.