34. Stream I/O in Java
Indice
- 34.1 Che cos’è uno Stream I/O in Java
- 34.2 Stream di Byte vs Stream di Caratteri
- 34.3 Stream di Basso Livello vs Stream di Alto Livello
- 34.3.1 Stream di Basso Livello Node-Streams
- 34.3.2 Stream comuni di Basso Livello
- 34.3.3 Stream di Alto Livello Filter--Processing-Streams
- 34.3.4 Stream comuni di Alto Livello
- 34.3.5 Regole di chaining degli stream e errori comuni
- 34.3.5.1 Regola fondamentale di chaining
- 34.3.5.2 Incompatibilità tra stream di byte e stream di caratteri
- 34.3.5.3 Chaining non valido errore-di-compilazione
- 34.3.5.4 Bridging da stream di byte a stream di caratteri
- 34.3.5.5 Pattern corretto di conversione
- 34.3.5.6 Regole di ordinamento nelle catene di stream
- 34.3.5.7 Ordine logico corretto
- 34.3.5.8 Regola di gestione delle risorse
- 34.3.5.9 Trappole comuni
- 34.4 Classi base principali di javaio e metodi chiave
- 34.5 Stream bufferizzati e prestazioni
- 34.6 java io vs java nio e java nio file
- 34.7 Quando usare quale API
- 34.8 Trappole comuni e suggerimenti
Questo capitolo fornisce una spiegazione dettagliata degli stream I/O in Java.
Copre gli stream classici java.io, li mette a confronto con java.nio / java.nio.file, e spiega principi di progettazione, API, casi limite e distinzioni rilevanti.
34.1 Che cos’è uno Stream I/O in Java?
Uno stream I/O rappresenta un flusso di dati tra un programma Java e una sorgente o destinazione esterna.
I dati scorrono in modo sequenziale, come acqua in un tubo.
- Uno stream non è una struttura dati; non memorizza dati in modo permanente
- Gli stream sono unidirezionali (input O output)
- Gli stream astraggono la sorgente sottostante (file, rete, memoria, dispositivo)
- Gli stream operano in modo bloccante, sincrono (I/O classico)
In Java, gli stream sono organizzati attorno a due dimensioni principali:
Direzione: Input vs OutputTipo di dato: Byte vs Carattere
34.2 Stream di Byte vs Stream di Caratteri
Java distingue gli stream in base all’unità di dato che elaborano.
34.2.1 Stream di Byte
- Lavorano con byte grezzi a 8 bit
- Usati per dati binari (immagini, audio, PDF, ZIP)
- Classi base:
InputStreameOutputStream
34.2.2 Stream di Caratteri
- Lavorano con caratteri Unicode a 16 bit
- Gestiscono automaticamente l’encoding dei caratteri
- Classi base:
ReadereWriter
34.2.3 Tabella di riepilogo
| Aspetto | Stream di Byte | Stream di Caratteri |
|---|---|---|
Unità di dato |
byte (8 bit) | char (16 bit) |
Gestione encoding |
Nessuna | Sì (consapevole del charset) |
Classi base |
InputStream / OutputStream | Reader / Writer |
Uso tipico |
File binari | File di testo |
Focus |
I/O a basso livello | Elaborazione testo |
34.3 Stream di Basso Livello vs Stream di Alto Livello
Gli stream in java.io seguono un pattern decorator. Gli stream vengono impilati per aggiungere funzionalità.
34.3.1 Stream di Basso Livello (Node Streams)
Gli stream di basso livello si collegano direttamente a una sorgente o a una destinazione di dati.
- Sanno come leggere/scrivere byte o caratteri
- NON forniscono buffering, formattazione o gestione di oggetti
34.3.2 Stream comuni di Basso Livello
| Classe Stream | Scopo |
|---|---|
FileInputStream |
Legge byte da file |
FileOutputStream |
Scrive byte su file |
FileReader |
Legge caratteri da file |
FileWriter |
Scrive caratteri su file |
- Esempio: stream di byte a basso livello
try (InputStream in = new FileInputStream("data.bin")) {
int b;
while ((b = in.read()) != -1) {
System.out.println(b);
}
}
Note
Gli stream di basso livello sono raramente usati da soli nelle applicazioni reali a causa di prestazioni scarse e funzionalità limitate.
34.3.3 Stream di Alto Livello (Filter / Processing Streams)
Gli stream di alto livello avvolgono altri stream per aggiungere funzionalità.
- Buffering
- Conversione del tipo di dato
- Serializzazione di oggetti
- Lettura/scrittura di primitivi
34.3.4 Stream comuni di Alto Livello
| Classe Stream | Aggiunge funzionalità |
|---|---|
BufferedInputStream |
Buffering |
BufferedReader |
Lettura basata su linee |
DataInputStream |
Tipi primitivi |
ObjectInputStream |
Serializzazione oggetti |
PrintWriter |
Output testo formattato |
- Esempio: chaining degli stream
try (BufferedReader reader =
new BufferedReader(
new InputStreamReader(
new FileInputStream("text.txt")))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
34.3.5 Regole di chaining degli stream e errori comuni
L’esempio precedente illustra lo stream chaining, un concetto centrale in java.io basato sul pattern decorator.
Ogni stream avvolge un altro stream, aggiungendo funzionalità preservando una gerarchia di tipi rigorosa.
34.3.5.1 Regola fondamentale di chaining
Uno stream può avvolgere solo un altro stream di un livello di astrazione compatibile.
- Gli stream di byte possono avvolgere solo stream di byte
- Gli stream di caratteri possono avvolgere solo stream di caratteri
- Gli stream di alto livello richiedono uno stream di basso livello sottostante
Note
Non puoi mescolare arbitrariamente InputStream con Reader o OutputStream con Writer.
34.3.5.2 Incompatibilità tra stream di byte e stream di caratteri
Un errore molto comune è tentare di avvolgere uno stream di byte direttamente con una classe basata su caratteri (o viceversa).
34.3.5.3 Chaining non valido (errore di compilazione)
BufferedReader reader =
new BufferedReader(new FileInputStream("text.txt"));
Note
Questo fallisce perché BufferedReader si aspetta un Reader, non un InputStream.
34.3.5.4 Bridging da stream di byte a stream di caratteri
Per convertire tra stream basati su byte e stream basati su caratteri, Java fornisce classi ponte che eseguono decodifica/codifica esplicita del charset.
InputStreamReaderconverte byte → caratteriOutputStreamWriterconverte caratteri → byte
34.3.5.5 Pattern corretto di conversione
BufferedReader reader =
new BufferedReader(
new InputStreamReader(new FileInputStream("text.txt")));
Note
Il ponte gestisce la decodifica dei caratteri usando un charset (predefinito o esplicito).
34.3.5.6 Regole di ordinamento nelle catene di stream
L’ordine di wrapping non è arbitrario.
- Lo stream di basso livello deve essere il più interno
- I bridge (se necessari) vengono dopo
- Gli stream bufferizzati o di elaborazione vengono per ultimi
34.3.5.7 Ordine logico corretto
FileInputStream → InputStreamReader → BufferedReader
34.3.5.8 Regola di gestione delle risorse
Chiudere lo stream più esterno chiude automaticamente tutti gli stream avvolti.
Note
Per questo try-with-resources dovrebbe riferirsi solo allo stream di livello più alto.
34.3.5.9 Trappole comuni
- Tentare di bufferizzare uno stream del tipo sbagliato
- Dimenticare il bridge tra stream di byte e stream di char
- Assumere che
Readerfunzioni con dati binari - Usare il charset predefinito involontariamente
- Chiudere manualmente gli stream interni (rischiando double-close):
close()sul wrapper esterno è sufficiente ed è raccomandato
34.4 Classi base principali di java.io e metodi chiave
Il package java.io è costruito attorno a un piccolo insieme di classi base astratte.
Comprendere queste classi e i loro contratti è essenziale, perché tutte le classi I/O concrete si basano su di esse.
34.4.1 InputStream
Classe base astratta per input orientato ai byte. Tutti gli input stream leggono byte grezzi (valori a 8 bit) da una sorgente come un file, un socket di rete o un buffer di memoria.
34.4.1.1 Metodi chiave
| Metodo | Descrizione |
|---|---|
int read() |
Legge un byte (0–255); ritorna -1 a fine stream |
int read(byte[]) |
Legge byte in un buffer; ritorna numero di byte letti o -1 |
int read(byte[], int, int) |
Legge fino a length byte in una slice del buffer |
int available() |
Byte leggibili senza bloccare (hint, non garanzia) |
void close() |
Rilascia la risorsa sottostante |
Note
I metodi read() sono bloccanti per default.
Sospendono il thread chiamante finché i dati non sono disponibili, finché non si raggiunge end-of-stream, o finché non si verifica un errore I/O.
Il metodo read() a singolo byte è principalmente un primitivo di basso livello.
In pratica, leggere un byte alla volta è inefficiente e dovrebbe quasi sempre essere evitato a favore di letture bufferizzate.
34.4.1.2 Esempio tipico di utilizzo
try (InputStream in = new FileInputStream("data.bin")) {
byte[] buffer = new byte[1024];
int count;
while ((count = in.read(buffer)) != -1) {
// process buffer[0..count-1]
}
}
34.4.2 OutputStream
Classe base astratta per output orientato ai byte.
Rappresenta una destinazione dove possono essere scritti byte grezzi.
34.4.2.1 Metodi chiave
| Metodo | Descrizione |
|---|---|
void write(int b) |
Scrive gli 8 bit meno significativi dell’intero |
void write(byte[]) |
Scrive un intero array di byte |
void write(byte[], int, int) |
Scrive una slice di un array di byte |
void flush() |
Forza la scrittura dei dati bufferizzati |
void close() |
Esegue flush e rilascia la risorsa |
Note
Chiamare close() richiama implicitamente flush().
Non eseguire flush o close su un OutputStream può causare perdita di dati.
34.4.2.2 Esempio tipico di utilizzo
try (OutputStream out = new FileOutputStream("out.bin")) {
out.write(new byte[] {1, 2, 3, 4});
out.flush();
}
34.4.3 Reader e Writer
Reader e Writer sono le controparti orientate ai caratteri di InputStream e OutputStream.
Operano su caratteri Unicode a 16 bit invece di byte grezzi.
| Classe | Direzione | Basata su caratteri | Consapevole dell’encoding |
|---|---|---|---|
Reader |
Input | Sì | Sì |
Writer |
Output | Sì | Sì |
Reader e Writer implicano sempre un charset, esplicitamente o implicitamente.
Questo li rende l’astrazione corretta per l’elaborazione di testo.
34.4.3.1 Gestione del charset
Reader reader = new InputStreamReader(
new FileInputStream("file.txt"),
StandardCharsets.UTF_8
);
Note
InputStreamReader e OutputStreamWriter sono classi ponte.
Convertono tra stream di byte e stream di caratteri usando un charset.
34.5 Stream bufferizzati e prestazioni
Gli stream bufferizzati avvolgono un altro stream e aggiungono un buffer in memoria.
Invece di interagire con il sistema operativo a ogni read o write, i dati vengono accumulati in memoria e trasferiti in blocchi più grandi.
BufferedInputStream/BufferedOutputStreamper stream di byteBufferedReader/BufferedWriterper stream di caratteri
Note
Gli stream bufferizzati sono decorator: non sostituiscono lo stream sottostante, lo migliorano aggiungendo comportamento di buffering.
34.5.1 Perché il buffering conta
| Aspetto | Non bufferizzato | Bufferizzato |
|---|---|---|
System calls |
Frequenti | Ridotte |
Prestazioni |
Scarse | Alte |
Uso memoria |
Minimo | Leggermente più alto |
Le system call sono operazioni costose.
Il buffering le minimizza raggruppando più letture o scritture logiche in meno operazioni I/O fisiche.
34.5.2 Come funziona la lettura non bufferizzata
In uno stream non bufferizzato, ogni chiamata a read() può risultare in una system call nativa.
Questo è particolarmente inefficiente quando si leggono grandi quantità di dati.
try (InputStream in = new FileInputStream("data.bin")) {
int b;
while ((b = in.read()) != -1) {
// ogni read() può innescare una system call
}
}
Note
Leggere byte-per-byte senza buffering è quasi sempre un anti-pattern di prestazioni.
34.5.3 Come funziona BufferedInputStream
BufferedInputStream internamente legge un grande blocco di byte in un buffer.
Le successive chiamate read() sono servite direttamente dalla memoria finché il buffer non è vuoto.
try (InputStream in =
new BufferedInputStream(new FileInputStream("data.bin"))) {
int b;
while ((b = in.read()) != -1) {
// la maggior parte delle letture è servita dalla memoria, non dall’OS
}
}
Note
Il programma chiama ancora read() ripetutamente, ma il sistema operativo viene invocato solo quando il buffer interno deve essere riempito di nuovo.
34.5.4 Esempio di output bufferizzato
L’output bufferizzato accumula dati in memoria e li scrive in blocchi più grandi.
L’operazione flush() forza la scrittura immediata del buffer.
try (OutputStream out =
new BufferedOutputStream(new FileOutputStream("out.bin"))) {
for (int i = 0; i < 1_000; i++) {
out.write(i);
}
out.flush(); // forza i dati bufferizzati su disco
}
Note
close() chiama automaticamente flush().
Chiamare flush() esplicitamente è utile quando i dati devono essere visibili immediatamente.
34.5.5 BufferedReader vs Reader
BufferedReader aggiunge una **lettura basata su linee** efficiente sopra un Reader.
Senza buffering, ogni carattere letto può coinvolgere una system call.
try (BufferedReader reader =
new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
Note
Il metodo readLine() è disponibile solo su BufferedReader (non su Reader), perché si basa sul buffering per rilevare efficientemente i confini di riga.
34.5.6 Esempio di BufferedWriter
try (BufferedWriter writer =
new BufferedWriter(new FileWriter("file.txt"))) {
writer.write("Hello");
writer.newLine();
writer.write("World");
}
BufferedWriter minimizza l’accesso al disco e fornisce metodi di convenienza come newLine().
Note
Avvolgi sempre gli stream di file con buffering a meno che non ci sia una forte ragione per non farlo
Preferisci BufferedReader / BufferedWriter per testo
Preferisci BufferedInputStream / BufferedOutputStream per dati binari
34.6 java.io vs java.nio (e java.nio.file)
Le applicazioni Java moderne favoriscono sempre più le API NIO e NIO.2, ma java.io rimane fondamentale e ampiamente usato.
34.6.1 Differenze concettuali
| Aspetto | java.io | java.nio / nio.2 |
|---|---|---|
Modello di programmazione |
Basato su stream | Basato su buffer / channel |
I/O bloccante |
Bloccante per default | Capace di non-bloccante |
File API |
File | Path + Files |
Scalabilità |
Limitata | Alta |
Introdotto |
Java 1.0 | Java 4 / Java 7 |
Note
java.nio non sostituisce java.io.
Molte classi NIO internamente si basano su stream o coesistono con essi.
34.6.2 java.nio (I/O file moderno)
Il package java.nio.file (NIO.2) fornisce una file API di alto livello, espressiva e più sicura.
È l’approccio preferito per operazioni su file in Java 11+.
Esempio: leggere un file (NIO)
Path path = Path.of("file.txt");
List<String> lines = Files.readAllLines(path);
Codice java.io equivalente
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
34.7 Quando usare quale API
| Scenario | API raccomandata |
|---|---|
Lettura/scrittura file semplice |
java.nio.file.Files |
Streaming binario |
InputStream / OutputStream |
Elaborazione testo a caratteri |
Reader / Writer |
Server ad alte prestazioni |
java.nio.channels |
API legacy |
java.io |
34.8 Trappole comuni e suggerimenti
- End-of-file è indicato da -1, non da un’eccezione
- Chiudere uno stream wrapper chiude automaticamente lo stream avvolto
BufferedReader.readLine()rimuove i separatori di lineaInputStreamReadercoinvolge sempre un charset- I metodi utility Files lanciano IOException checked
available()non deve essere usato per rilevare EOF
Note
La maggior parte dei bug I/O deriva da assunzioni errate su blocking, buffering o character encoding.