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 loopreadAllBytes() è 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 markQui 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 resourcesflush() è 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:
| Classe | Cosa incapsula |
|---|---|
FileInputStream / FileOutputStream | Un file su disco. Apre un file descriptor. |
ByteArrayInputStream / ByteArrayOutputStream | Un byte[] in memoria. Utile per test e per catturare output. |
BufferedInputStream / BufferedOutputStream | Una vista bufferizzata di un altro stream. |
PipedInputStream / PipedOutputStream | Una pipe produttore/consumatore tra thread. |
DataInputStream / DataOutputStream | Sovrapposto 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.
Cosa trarre dall'esecuzione:
- Il lato della scrittura ha usato
Files.newOutputStream— una factory in stileFilesche restituisce un sempliceOutputStream. Una volta ottenuto, l'API è la stessa che Java ha avuto dall'1.0. La factory risparmia solo la costruzione diFileOutputStreame la preoccupazione per le opzioni di apertura. - Il ciclo di lettura ha usato
n, nonbuf.length, quando ha chiamatocrc.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. Usarebuf.lengthavrebbe 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 ciclowhile ((n = in.read(buf)) != -1)senza altra logica al suo interno.ByteArrayOutputStreamsi è collegato direttamente atransferTo. Sembra un file ma vive in memoria — la stessa API. Questa simmetria è ciò che rendejava.iotestabile: passa unByteArrayInputStreamcome sorgente, unByteArrayOutputStreamcome sink, e puoi fare unit test del codice che "scrive su un file" senza toccare il disco.- Il blocco finale ha stampato
255poi-1. Questo è il contratto:0xFFè un valore byte legale e viene riletto come255;-1è il sentinella fuori banda che dice "nessun altro byte." Trattare il valore restituito comebyte(invece diint) e confrontare con== -1tratterebbe silenziosamente un vero0xFFcome fine dello stream. Memorizza sempre il risultato in uninte confrontalo con-1prima 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".