W3docs

Java Byte Stream

Leggi e scrivi dati binari in Java con InputStream, OutputStream, FileInputStream e FileOutputStream.

Il capitolo 1 ha introdotto il design di java.io come uno stack di decorator: uno stream grezzo alla base, strati di funzionalità avvolti attorno ad esso, con lo strato più alto che espone l'API che chiami. I primi sei capitoli di questa parte vivono in cima a quello stack — Files.readString, Files.lines, Files.writeString. Questo capitolo scende di un livello verso l'astrazione orientata ai byte su cui è costruito l'intero stack: InputStream e OutputStream.

Ogni file, socket, pipe e buffer in memoria in java.io è — alla base — uno stream di byte. Anche un file di testo UTF-8 è byte su disco; la visione "questo è testo" proviene da un Reader sovrapposto a un InputStream. Conoscere l'API dei byte è importante quando i dati non sono testo (immagini, audio, archivi, protocolli di rete), quando è necessario copiare byte senza decodificarli, e quando si vuole capire cosa stanno realmente facendo le API di livello superiore.

Il contratto di InputStream

InputStream è una classe astratta con un solo metodo. Quel metodo è:

public abstract int read() throws IOException;

Restituisce il prossimo byte come int nell'intervallo 0..255, oppure -1 quando lo stream è esaurito. L'int non è un errore: un byte in Java è con segno (-128..127), ma il contratto dello stream è senza segno, quindi il tipo restituito più ampio rende "fine dello stream" (-1) distinguibile da un vero valore byte (0xFF viene letto come 255, non come -1).

Altri tre metodi sono definiti sopra read() e sono quelli che di solito si chiamano:

int read(byte[] buf);                  // read up to buf.length bytes; return count or -1
int read(byte[] buf, int off, int len); // same, into a slice
byte[] readAllBytes();                  // Java 9+: read everything into a byte[]
long transferTo(OutputStream out);       // Java 9+: pipe straight to a sink, no copy loop

readAllBytes() è la comodità per i file piccoli; transferTo è la comodità per copiare senza decodificare. Per tutto il resto c'è il ciclo di lettura bufferizzata, che è la forma canonica:

byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
  out.write(buf, 0, n);                 // n bytes, not buf.length — the last chunk is short
}

Due cose da interiorizzare. Prima, le chiamate read(byte[]) restituiscono quanti byte sono stati effettivamente letti, non sempre buf.length. L'ultima lettura è quasi sempre parziale; trattare il buffer come pieno corrompe i dati. Secondo, read() e read(byte[]) sono bloccanti — ritornano quando è disponibile almeno un byte o lo stream termina. Non ritornano anticipatamente su un disco lento o un socket lento.

Saltare, spiare e riavvolgere

InputStream definisce anche tre metodi che si usano meno spesso ma che è bene riconoscere:

long skip(long n);     // discard up to n bytes without copying them anywhere
int  available();      // bytes you can read right now without blocking — an estimate, not a length
boolean markSupported();
void mark(int readAheadLimit);  // remember this position
void reset();                    // jump back to the last mark

Qui ci sono due trappole. available() non è la dimensione dello stream — per un file spesso lo è, ma per un socket è "byte già bufferizzati", che può essere 0 durante un trasferimento. Non scrivere mai new byte[in.available()] assumendo di leggere tutto. E mark/reset funzionano solo se markSupported() restituisce true; un FileInputStream grezzo restituisce false, quindi avvolgilo in un BufferedInputStream (capitolo successivo) quando hai bisogno di guardare avanti e tornare indietro.

Il contratto di OutputStream

La classe speculare è OutputStream, anch'essa con un solo metodo astratto:

public abstract void write(int b) throws IOException;

Scrive gli 8 bit bassi di b e ignora il resto. Gli overload di comodità sono:

void write(byte[] buf);                    // write the whole array
void write(byte[] buf, int off, int len);  // write a slice — this is the one you usually want
void flush();                               // push buffered data to the OS
void close();                               // flush + release resources

flush() è rilevante solo se lo stream bufferizza. Il FileOutputStream grezzo non bufferizza — ogni write chiama l'OS — quindi flush è un no-op. BufferedOutputStream (capitolo successivo) è dove vivono la bufferizzazione e la necessità di fare flush.

close() chiama prima flush(). Ecco perché "dimenticarsi di chiudere lo stream bufferizzato" tronca silenziosamente il file: il buffer finale è in memoria in attesa di un flush che non arriva mai.

Stream di byte concreti

Le sottoclassi concrete che istanzierai davvero:

ClasseCosa incapsula
FileInputStream / FileOutputStreamUn file su disco. Apre un file descriptor.
ByteArrayInputStream / ByteArrayOutputStreamUn byte[] in memoria. Utile per test e per catturare output.
BufferedInputStream / BufferedOutputStreamUna vista bufferizzata di un altro stream.
PipedInputStream / PipedOutputStreamUna pipe produttore/consumatore tra thread.
DataInputStream / DataOutputStreamSovrapposto a uno stream di byte per leggere/scrivere primitivi in modo portabile.

FileInputStream e FileOutputStream sono gli stream file grezzi. Sono non bufferizzati: ogni read()/write() è una syscall. Questo è catastrofico per i cicli byte per byte — milioni di syscall — ed è appena accettabile per letture a blocchi con un buffer da 8 KB o più grande. Il capitolo sulla bufferizzazione è ciò che rende accessibile l'API byte per byte.

// Raw, unbuffered — fine for chunked reads
try (FileInputStream in = new FileInputStream("photo.jpg")) {
  byte[] buf = new byte[8192];
  int n;
  while ((n = in.read(buf)) != -1) { /* process buf[0..n] */ }
}

// Equivalent one-liner, Java 7+
byte[] all = Files.readAllBytes(Path.of("photo.jpg"));

Files.readAllBytes è la chiamata giusta per i file piccoli; per tutto ciò che potrebbe non entrare in memoria, il ciclo a blocchi è la forma sicura.

Tre pattern da memorizzare

Le tre cose che fai continuamente con gli stream di byte:

// 1. Copy a file
try (InputStream in  = Files.newInputStream(src);
     OutputStream out = Files.newOutputStream(dst)) {
  in.transferTo(out);                                 // Java 9+: no manual loop
}
// Java 7+ one-liner: Files.copy(src, dst);

// 2. Read everything into memory
byte[] all = Files.readAllBytes(path);                 // small-file shortcut

// 3. Build a byte[] you don't know the size of in advance
ByteArrayOutputStream baos = new ByteArrayOutputStream();
in.transferTo(baos);
byte[] bytes = baos.toByteArray();

ByteArrayOutputStream è il sink di byte "cresce al volo". È il modo in cui il JDK stesso implementa readAllBytes() su stream la cui lunghezza non è nota in anticipo. Non lancia mai eccezioni su write (fino a esaurimento dell'heap) e non ha semantiche close() degne di attenzione, il che lo rende il fixture standard per i test di "cattura ciò che questo writer ha prodotto."

Quando usare gli stream di byte

La risposta onesta: quando i dati non sono testo. Qualsiasi cosa binaria — immagini, audio, video, archivi (.zip, .tar), eseguibili, protocol buffer, formati di file personalizzati — è byte e rimane byte.

Quando i dati sono testo, preferisci il lato degli stream di caratteri (Reader/Writer, capitolo successivo) o i moderni Files.readString / Files.lines. Leggere un file di testo come byte grezzi e decodificarlo a mano è il modo standard per inventare il proprio bug di charset — i caratteri multi-byte UTF-8 vengono divisi tra le chiamate read() e vengono riassemblati in modo errato. Il livello Reader esiste proprio perché non devi pensarci.

Un esempio pratico: copiare, fare hash e catturare

Il programma seguente esercita l'API degli stream di byte da cima a fondo. Scrive un piccolo file binario (un header più un payload), lo rilegge pezzo per pezzo calcolando un checksum, lo copia in un secondo file con transferTo, e cattura un'altra copia in un ByteArrayOutputStream così puoi vedere il sink in memoria in azione. I file temporanei si puliscono da soli all'uscita.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • Il lato della scrittura ha usato Files.newOutputStream — una factory in stile Files che restituisce un semplice OutputStream. Una volta ottenuto, l'API è la stessa che Java ha avuto dall'1.0. La factory risparmia solo la costruzione di FileOutputStream e la preoccupazione per le opzioni di apertura.
  • Il ciclo di lettura ha usato n, non buf.length, quando ha chiamato crc.update. Il motivo è nella riga di output: "read in N chunks." Il buffer era di 256 byte e il file era di 1004 byte, quindi l'ultimo blocco era corto. Usare buf.length avrebbe calcolato l'hash di byte spazzatura oltre i dati reali.
  • in.transferTo(out) è il ciclo di copia testato del JDK. È misurabilmente più veloce di un ciclo scritto a mano sulla maggior parte delle JVM perché può usare un buffer da 16 KB e saltare i controlli del safepoint, ed è una riga invece di cinque. Usalo ogni volta che altrimenti scriveresti un ciclo while ((n = in.read(buf)) != -1) senza altra logica al suo interno.
  • ByteArrayOutputStream si è collegato direttamente a transferTo. Sembra un file ma vive in memoria — la stessa API. Questa simmetria è ciò che rende java.io testabile: passa un ByteArrayInputStream come sorgente, un ByteArrayOutputStream come sink, e puoi fare unit test del codice che "scrive su un file" senza toccare il disco.
  • Il blocco finale ha stampato 255 poi -1. Questo è il contratto: 0xFF è un valore byte legale e viene riletto come 255; -1 è il sentinella fuori banda che dice "nessun altro byte." Trattare il valore restituito come byte (invece di int) e confrontare con == -1 tratterebbe silenziosamente un vero 0xFF come fine dello stream. Memorizza sempre il risultato in un int e confrontalo con -1 prima di eseguire il cast.

Cosa c'è dopo

I byte sono l'astrazione giusta per i dati binari. Il prossimo capitolo, Java Character Streams, copre la gerarchia parallela per il testo — Reader e Writer, il bridging del charset, e perché "new FileReader(path)" è la classica fonte dei bug "funziona sulla mia macchina, rotto sul server".

Esercizi

Pratica
Cosa restituisce `InputStream.read()` quando lo stream contiene un singolo byte con valore `0xFF`, e cosa restituisce alla chiamata successiva?
Cosa restituisce `InputStream.read()` quando lo stream contiene un singolo byte con valore `0xFF`, e cosa restituisce alla chiamata successiva?
Pratica
Nel ciclo `while ((n = in.read(buf)) != -1) out.write(buf, 0, n);`, perché passare `n` invece di `buf.length` a `write`?
Nel ciclo `while ((n = in.read(buf)) != -1) out.write(buf, 0, n);`, perché passare `n` invece di `buf.length` a `write`?
Was this page helpful?