W3docs

Java Buffered Streams

Velocizza l'I/O in Java con gli stream bufferizzati: BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream.

I capitoli sugli stream di byte e di caratteri hanno descritto onestamente le API grezze: ogni chiamata a FileInputStream.read() o FileReader.read() è una syscall. Una syscall richiede circa un microsecondo — veloce in isolamento, catastrofica in un ciclo stretto. Leggere un file da 1 MB un byte alla volta significa un milione di syscall; lo stesso file con un buffer da 8 KB ne richiede solo 128. La differenza in termini di tempo reale è di due o tre ordini di grandezza.

I decoratori Buffered* si interpongono tra il tuo codice e lo stream grezzo. Mantengono un byte[] (o char[]) in memoria e servono le chiamate read() da lì, andando al sistema operativo solo quando il buffer è vuoto. Lato scrittura, accumulano piccole scritture nel buffer e le inviano al SO solo quando il buffer è pieno o chiami flush/close. Stessa API, costo completamente diverso.

Le quattro classi bufferizzate

ClasseRacchiude
BufferedInputStreamUn InputStream. Aggiunge un buffer interno byte[].
BufferedOutputStreamUn OutputStream. Aggiunge un buffer interno byte[].
BufferedReaderUn Reader. Aggiunge un buffer interno char[] e il famoso metodo readLine().
BufferedWriterUn Writer. Aggiunge un buffer interno char[] e un metodo newLine().

Tutte e quattro racchiudono qualsiasi stream del tipo corrispondente — file, socket, pipe, in memoria — non solo stream di file:

BufferedInputStream  in  = new BufferedInputStream(new FileInputStream(path.toFile()));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(path.toFile()));
BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));

La dimensione predefinita del buffer è 8192 byte/char — scelta per corrispondere alle dimensioni comuni delle pagine del SO. Puoi passare una dimensione diversa al secondo costruttore, ma quella predefinita va benissimo in quasi tutti i casi. Buffer più grandi non aumentano la velocità in modo lineare; consumano semplicemente più memoria.

La moderna API fornisce questi decoratori già assemblati:

BufferedReader r = Files.newBufferedReader(path);                            // UTF-8 by default
BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8);
InputStream    in  = new BufferedInputStream(Files.newInputStream(path));
OutputStream   out = new BufferedOutputStream(Files.newOutputStream(path));

Files.newBufferedReader / Files.newBufferedWriter racchiudono già la classe bridge con il charset corretto e un BufferedReader/BufferedWriter. Per il testo, questo sostituisce con una sola riga lo stack manuale profondo tre livelli.

BufferedReader.readLine()

Il motivo per cui BufferedReader è la classe più utilizzata in java.io:

String readLine() throws IOException;          // a line, terminator stripped, or null at end
Stream<String> lines();                         // Java 8+: line stream

readLine riconosce \n, \r e \r\n come terminatori di riga e restituisce la riga senza il terminatore. Restituisce null (non una stringa vuota, non -1) alla fine dello stream — l'idioma standard per la lettura riga per riga:

try (BufferedReader r = Files.newBufferedReader(path)) {
  String line;
  while ((line = r.readLine()) != null) {
    process(line);
  }
}

r.lines() restituisce un Stream<String> per la forma con pipeline funzionale. Lo stream possiede il Reader aperto, quindi il try-with-resources attorno al reader gestisce comunque la chiusura — lines() stesso non ha bisogno di una propria chiusura.

Due cose da sapere su readLine(). Prima: alloca una String per ogni riga. Per cicli intensivi di elaborazione di log in cui l'allocazione è critica, la soluzione migliore è il read(char[]) di livello inferiore. Seconda: una riga vuota è "" (una stringa vuota), non null — il file termina solo quando readLine() restituisce null.

BufferedWriter.newLine()

Il corrispondente comodo lato scrittura:

void newLine() throws IOException;             // platform line separator: \n on Unix, \r\n on Windows

newLine() scrive qualunque separatore di riga la JVM consideri corretto per la piattaforma corrente. Questo è un vantaggio se stai producendo file per occhi umani sulla macchina locale; è un bug se stai producendo file di dati, file di log, o qualsiasi cosa destinata a un'altra macchina. Internet funziona con \n. Scrivi sempre \n esplicitamente quando l'output deve essere portabile:

w.write("line one\n");                          // portable
w.newLine();                                    // platform-dependent: \n on Unix, \r\n on Windows

Lo stesso consiglio vale per PrintWriter.println e il formato %n — dipendono dalla piattaforma. Usali solo quando l'output è per il consumo locale.

La trappola del "buffer finale mai svuotato"

Questo è il bug che ogni codebase Java incontra almeno una volta:

// WRONG
BufferedWriter w = Files.newBufferedWriter(path);
w.write("hello");
return;                                          // 'hello' is sitting in the buffer; nothing on disk

Un BufferedWriter non invia byte al SO finché il buffer non è pieno o close() non viene eseguito. Saltare la chiusura significa perdere la coda — Files.size(path) è 0 e non si capisce perché. La soluzione è usare try-with-resources ogni singola volta:

try (BufferedWriter w = Files.newBufferedWriter(path)) {
  w.write("hello");
}                                                // close() runs here; tail is flushed

Se hai bisogno dei dati su disco prima della chiusura — un watcher della coda del log, o un altro processo che esegue il polling del file — chiama flush() esplicitamente. Il buffer non si svuota automaticamente dopo ogni scrittura; questo è il prezzo dell'avere un buffer.

Mark e reset

Sia BufferedReader che BufferedInputStream supportano una piccola API di "guarda avanti e torna indietro":

in.mark(1024);                                   // remember this position; allow up to 1024 bytes of lookahead
int b = in.read();
in.reset();                                      // back to the marked position

Questa è l'unica API di java.io che ti permette di leggere un byte/char e poi rimetterlo al suo posto. È il fondamento del codice che "legge i primi byte per capire il formato" — rilevamento del BOM UTF-8, analisi dei magic number, passaggio tra parser. Senza bufferizzazione non è possibile farlo: gli stream grezzi non conservano più i byte una volta letti.

Quando la bufferizzazione non aiuta

Due casi in cui aggiungere un decoratore Buffered* non porta alcun vantaggio:

  • La sorgente è già in memoria. ByteArrayInputStream e StringReader servono già read() da un byte[]/String in memoria; non ci sono syscall da ammortizzare.
  • Stai usando Files.readString, Files.readAllBytes, Files.write, o transferTo. Quelle chiamate fanno già il loro I/O a blocchi con un grande buffer interno. Racchiuderle in BufferedInputStream è ridondante — il JDK ha già bufferizzato.

Il caso in cui la bufferizzazione aiuta è quello originale: stai leggendo o scrivendo piccoli pezzi (un singolo byte, una singola riga, una chiamata printf) e la sorgente/destinazione è un file reale, un socket o una pipe.

Un esempio pratico: lo stesso carico, con e senza

Il programma seguente copia lo stesso blob da 32 KB byte per byte da un file temporaneo a un altro — una volta con FileInputStream/FileOutputStream grezzi, una volta con BufferedInputStream/BufferedOutputStream, una volta con transferTo come riferimento. I tempi misurati rendono visibile il costo del buffer mancante. L'esempio legge poi le righe del file tramite un BufferedReader e dimostra la trappola del "flush dimenticato" lato scrittura.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • La copia grezza byte per byte è stata di ordini di grandezza più lenta di quella bufferizzata. Il corpo del ciclo era identico; l'unica modifica è stata racchiudere gli stream di file in BufferedInputStream/BufferedOutputStream. Questo è l'unico motivo per cui questi decoratori esistono — stessa API, un numero di syscall enormemente ridotto.
  • transferTo è stato veloce quanto la versione bufferizzata (o più veloce). Per "copiare byte da A a B senza trasformazione," transferTo è quello che vuoi — bufferizza già internamente e il JDK ha ottimizzato il ciclo. Usalo prima di scrivere il tuo.
  • Files.newBufferedReader ha restituito direttamente un BufferedReader. Nota che non abbiamo mai scritto new BufferedReader(new InputStreamReader(new FileInputStream(...), UTF_8)) — quello stack a tre livelli è ciò che la factory nasconde. readLine() è venuto fuori da quello stack gratuitamente.
  • Lo scrittore che perde dati ha stampato 0 bytes prima di flush(). Quei caratteri erano nel buffer in memoria, non su disco. Chiamare flush() li ha inviati; senza il flush esplicito (o una close() corretta), sarebbero andati persi. Ecco perché try-with-resources attorno ai buffered writer non è opzionale — è il contratto che rende la scrittura visibile.
  • Il ciclo BufferedReader.readLine() è la forma di elaborazione testo più comune in Java. Memorizza la forma while ((line = r.readLine()) != null): l'assegnazione nella condizione è idiomatica qui, e il sentinella null (non una stringa vuota) è la condizione di uscita del ciclo.

Cosa c'è dopo

La bufferizzazione risolve il costo di una syscall per chiamata ma non cambia cosa significano i byte. Il prossimo capitolo, Java DataInput and DataOutput Streams, tratta i decoratori che leggono e scrivono i primitivi Java in un formato binario portabile — il livello che ti permette di scrivere un int su un file e rileggerlo come int su un SO diverso.

Pratica

Pratica
Cosa succede ai dati scritti da `w.write('hello')` se dimentichi di chiudere un `BufferedWriter` (e non chiami mai `flush()`)?
Cosa succede ai dati scritti da `w.write('hello')` se dimentichi di chiudere un `BufferedWriter` (e non chiami mai `flush()`)?
Was this page helpful?