W3docs

Stream di caratteri in Java

Leggi e scrivi testo in Java con Reader, Writer, FileReader, FileWriter e la gestione della codifica dei caratteri.

Il capitolo precedente trattava i byte stream — il livello grezzo in cui tutto è byte. Quel livello è adatto per i dati binari e inadatto per il testo. Un carattere UTF-8 può occupare uno, due, tre o quattro byte; UTF-16 usa unità di codice a due byte con coppie surrogate per tutto ciò che supera il piano multilingue base; persino il testo ASCII richiede da qualche parte una decisione del tipo "questo è ASCII". Chiamare InputStream.read() su testo e convertire il risultato in char funziona solo per fortuna, se il file ha un byte per carattere — e nel momento in cui qualcuno scrive "é", "日" o "🎉", la versione fortunata corrompe i dati.

La gerarchia dei character stream esiste per tenere quella decodifica fuori dal tuo codice. Reader e Writer lavorano con char, non con byte. Le classi ponte — InputStreamReader e OutputStreamWriter — accettano un Charset ed eseguono la conversione. Se il charset è corretto al livello ponte, ogni strato superiore lavora con testo decodificato.

Il contratto di Reader

Reader è il corrispettivo di InputStream, con un'astrazione su una coppia di metodi (read(char[], int, int) e close()) e alcune comodità in cima:

int read();                            // next char as int 0..65535, or -1 at end
int read(char[] buf);                  // read up to buf.length chars; return count or -1
int read(char[] buf, int off, int len); // into a slice
String readLine();                       // only on BufferedReader — not on Reader itself
long transferTo(Writer out);             // Java 10+: pipe straight to a sink

Due differenze sottili rispetto al lato byte. Prima: l'unità è char (un'unità di codice UTF-16 a 16 bit), non byte. Seconda: read() restituisce 0..65535 per un'unità di codice e -1 alla fine dello stream — la stessa tecnica sentinella di InputStream, ma con un intervallo legale più ampio.

Un char non è sempre un "carattere" — i caratteri al di fuori del piano multilingue base (U+10000 e superiori: la maggior parte delle emoji, scritture antiche) usano due unità di codice UTF-16 (una coppia surrogate). Se si divide per confini di char (ad esempio si leggono 100 char alla volta e si elaborano a blocchi) si può spezzare una coppia surrogate tra due letture. Per il testo orientato alle righe questo accade raramente; per l'elaborazione a livello di carattere di Unicode arbitrario, lavora con i code point (String.codePoints()).

Il contratto di Writer

Writer è il corrispettivo di OutputStream:

void write(int c);                          // low 16 bits
void write(char[] buf);
void write(char[] buf, int off, int len);
void write(String s);                        // convenience — encodes a whole String
void write(String s, int off, int len);
Writer append(CharSequence csq);             // chainable: w.append("a").append("b")
void flush();
void close();                                // calls flush() first

write(String) è la comodità che userai di più: la maggior parte dell'I/O testuale consiste in un piccolo numero di scritture grandi (un corpo JSON, un report generato) piuttosto che in un output carattere per carattere.

append esiste per l'interoperabilità con CharSequenceStringBuilder implementa CharSequence, quindi un Writer può essere la destinazione di codice che scrive in uno o nell'altro a seconda di un flag. È lo stesso metodo append che ha StringBuilder stesso, tramite interfaccia.

Stream di caratteri concreti

ClasseCosa racchiude
FileReader / FileWriterUn file su disco, decodificato come testo.
CharArrayReader / CharArrayWriterUn char[] in memoria.
StringReader / StringWriterUna String/StringBuilder in memoria.
BufferedReader / BufferedWriterUna vista bufferizzata di un altro Reader/Writer.
InputStreamReader / OutputStreamWriterClassi ponte: un Reader/Writer sopra un byte stream sottostante, con un Charset.
PrintWriterUn decoratore di Writer che aggiunge print, println e printf.

Le classi ponte sono il punto strutturale dell'intera gerarchia. Ogni character stream che comunica con un file, un socket o una pipe è — sotto — un byte stream più un charset. FileReader è un sottile wrapper attorno a InputStreamReader(new FileInputStream(...)); FileWriter analogamente attorno a OutputStreamWriter(new FileOutputStream(...)).

La trappola del charset

Il classico bug dell'I/O in Java:

// WRONG in any code that might run on more than one machine
try (FileReader in = new FileReader("data.txt")) { ... }
try (FileWriter out = new FileWriter("data.txt")) { ... }

I costruttori senza charset usano il charset predefinito della JVM, determinato all'avvio dalla locale del sistema operativo. Su un Mac per sviluppatori è quasi sempre UTF-8. Su un server Linux con locale C può essere US-ASCII. Su Windows con un'installazione inglese è Cp1252. Il bug "funziona sul mio Mac, rotto sul server di produzione" è esattamente questo costruttore.

Passa esplicitamente un charset:

// Right
try (FileReader in = new FileReader("data.txt", StandardCharsets.UTF_8)) { ... }
try (FileWriter out = new FileWriter("data.txt", StandardCharsets.UTF_8)) { ... }

(Le forme a due argomenti che accettano un Charset sono state aggiunte in Java 11. Prima bisognava ricorrere alle classi ponte — new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8) — e la riga con i decoratori annidati è uno dei motivi per cui è stato aggiunto Files.newBufferedReader(path): usa UTF-8 come default da Java 18 e prima era sempre esplicito sul charset.)

La moderna API Files ha reso questo default più sicuro:

String text = Files.readString(path);                // UTF-8 by default (Java 18+)
BufferedReader r = Files.newBufferedReader(path);    // UTF-8 by default (always was)

Se stai iniziando da zero, usa le factory di Files. Se stai modificando codice legacy con FileReader/FileWriter, il fix più economico è aggiungere il secondo argomento StandardCharsets.UTF_8.

Le classi ponte direttamente

Hai bisogno di InputStreamReader e OutputStreamWriter ogni volta che la sorgente non è un file — una ZipEntry, un socket, il corpo di una risposta HTTP, System.in, uno stream avvolto da Inflater — e vuoi testo in uscita:

// Read text from System.in as UTF-8
try (BufferedReader stdin = new BufferedReader(
        new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
  String line = stdin.readLine();
}

// Write the response of an HttpURLConnection as text
try (BufferedReader resp = new BufferedReader(
        new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
  resp.lines().forEach(System.out::println);
}

La struttura è sempre la stessa: byte stream → InputStreamReader(stream, charset)BufferedReader opzionale → il tuo codice.

Un esempio pratico: testo in tre forme

Il programma seguente scrive un piccolo file di testo UTF-8 contenente ASCII, caratteri accentati e un'emoji multi-byte, poi lo rilegge in quattro modi: come String, carattere per carattere, riga per riga tramite un BufferedReader, e tramite il costruttore legacy FileReader(charset). L'esempio mostra anche la struttura con le classi ponte che operano su un ByteArrayInputStream, così puoi vedere dove Reader e InputStream si incontrano.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • Il file su disco (23 byte) era più grande di content.length() (20). La String ha length() == 20 (contando ogni \n e contando l'emoji 🎉 come due unità di codice UTF-16 — questo è ciò che misura un char in Java); UTF-8 codifica l'emoji come quattro byte ed é come due, quindi il conteggio dei byte è maggiore. In code point ce ne sono solo 19 — l'emoji è un code point ma due char. Lo stesso testo logico è un numero in char, un altro in byte, un altro in code point. Sapere quale si intende è metà dei bug di charset.
  • Il ciclo carattere per carattere ha riassemblato la stessa identica stringa. L'API Reader ha gestito per te la decodifica UTF-8: una singola emoji compare come due chiamate a (char) read() a causa dei surrogati UTF-16, ma non hai mai dovuto pensare ai confini dei byte.
  • BufferedReader.readLine() ha restituito tre righe: hello, café, 🎉 party. Questo è il vocabolario orientato al testo — riga per riga, consapevole dei terminatori (gestisce \n, \r e \r\n), e costruito sopra la classe ponte. Ogni chiamata API in questo e nel prossimo capitolo si riduce in ultima analisi a "decodifica i byte attraverso un charset e fornisce caratteri."
  • Il blocco diretto InputStreamReader(new ByteArrayInputStream(raw), UTF_8) mostra la struttura: la sorgente byte all'interno, il charset al ponte, l'API dei caratteri all'esterno. Sostituisci ByteArrayInputStream con socket.getInputStream() e il resto è identico — ecco perché i client HTTP e JDBC convergono tutti sullo stesso idioma.
  • Il blocco finale ha decodificato gli stessi byte con il charset sbagliato. L'é accentata e l'emoji sono entrambe uscite come spazzatura — il classico bug mojibake. I byte su disco erano corretti; il charset al ponte era sbagliato. Ecco perché specificare il charset esplicitamente è la singola abitudine più utile nell'I/O di testo in Java.

Cosa viene dopo

Sia i byte stream che i character stream operano di default un elemento alla volta, e su un file stream grezzo ogni chiamata è una syscall. Il prossimo capitolo, Java Buffered Streams, tratta i decoratori Buffered* — un buffer in memoria tra il tuo codice e il sistema operativo — e l'API readLine() che vi risiede.

Pratica

Pratica
Perché `new FileReader(path)` e `new FileWriter(path)` (senza argomento charset) causano bug del tipo 'funziona sul mio computer, rotto sul server'?
Perché `new FileReader(path)` e `new FileWriter(path)` (senza argomento charset) causano bug del tipo 'funziona sul mio computer, rotto sul server'?
Was this page helpful?