1. Perché servono DataInputStream e DataOutputStream?
Quando lavori con i file, a volte è necessario memorizzare non solo testo, ma dati strutturati: numeri, valori booleani, array di primitivi. Per esempio, immagina di scrivere un semplice gioco e voler salvare i progressi dell’utente: numero di punti (int), livello attuale (int), tempo di gioco (double), stato di vincitore (boolean). Si può, certo, scrivere tutto in forma testuale:
12345
5
67.5
true
Ma è scomodo e poco sicuro: bisogna fare il parsing delle stringhe, il formato può «alterarsi» e i numeri occupano più spazio.
L’idea è quella di scrivere i dati su file in forma «grezza» (binaria), senza conversione in testo. Per questo in Java ci sono due classi ottime:
- DataOutputStream — sa scrivere i tipi primitivi in uno stream.
- DataInputStream — sa leggere i tipi primitivi da uno stream.
Funzionano sopra i normali stream di byte (OutputStream/InputStream). Sono cioè dei «wrapper» che non si limitano a scrivere byte, ma sanno come comporre da quei byte un int, un double, un boolean e persino una String.
Come funziona?
Un normale FileOutputStream si può immaginare come un nastro trasportatore su cui metti manualmente i byte uno dopo l’altro. Se vuoi scrivere un intero o una stringa, devi controllare da solo quanti byte occupa ogni elemento.
DataOutputStream semplifica la vita: agisce come un robot su quel nastro trasportatore. Gli dici «scrivi un numero» o «scrivi una stringa» e lui impacchetta i dati nel numero di byte necessario e li invia al disco. All’altra estremità del nastro un robot analogo — DataInputStream — sa ricostruire quei byte negli oggetti originari.
Perché è comodo? Perché non devi pensare al numero di byte per int, double o boolean. I dati sono compatti, si leggono e scrivono rapidamente e non c’è rischio di errori di parsing o problemi di formato.
2. Esempio: scrittura e lettura di primitivi
Supponiamo di voler salvare i risultati del nostro (ipotetico) gioco: nome del giocatore (String), numero di punti (int), tempo record (double), se il giocatore ha vinto (boolean).
Scrittura dei dati su file
import java.io.*;
public class SaveGameData {
public static void main(String[] args) {
String fileName = "savegame.bin";
String playerName = "Alice";
int score = 12345;
double recordTime = 67.5;
boolean isWinner = true;
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream(fileName))) {
dos.writeUTF(playerName); // Scriviamo la stringa (UTF-8)
dos.writeInt(score); // Scriviamo int (4 byte)
dos.writeDouble(recordTime); // Scriviamo double (8 byte)
dos.writeBoolean(isWinner); // Scriviamo boolean (1 byte)
System.out.println("Dati scritti correttamente nel file!");
} catch (IOException e) {
System.out.println("Errore di scrittura: " + e.getMessage());
}
}
}
- writeUTF(String) — scrive una stringa in formato UTF-8 (con la lunghezza all’inizio).
- writeInt(int) — scrive 4 byte.
- writeDouble(double) — scrive 8 byte.
- writeBoolean(boolean) — scrive 1 byte (1 oppure 0).
- Tutti i metodi «impacchettano» automaticamente i dati nel formato corretto.
Lettura dei dati dal file
import java.io.*;
public class LoadGameData {
public static void main(String[] args) {
String fileName = "savegame.bin";
try (DataInputStream dis = new DataInputStream(
new FileInputStream(fileName))) {
String playerName = dis.readUTF(); // Leggiamo la stringa
int score = dis.readInt(); // Leggiamo int
double recordTime = dis.readDouble(); // Leggiamo double
boolean isWinner = dis.readBoolean(); // Leggiamo boolean
System.out.println("Nome del giocatore: " + playerName);
System.out.println("Punti: " + score);
System.out.println("Tempo: " + recordTime);
System.out.println("Vincitore: " + isWinner);
} catch (IOException e) {
System.out.println("Errore di lettura: " + e.getMessage());
}
}
}
Punto importante:
L’ordine di lettura deve coincidere con l’ordine di scrittura! Se prima hai scritto una stringa, poi un int, poi un double — devi leggere nello stesso ordine. Altrimenti otterrai un errore o dati «confusi».
3. Quali tipi sono supportati?
DataOutputStream e DataInputStream supportano tutti i principali tipi primitivi di Java:
| Metodo di scrittura | Metodo di lettura | Tipo di dato | Dimensione (byte) |
|---|---|---|---|
|
|
boolean | 1 |
|
|
byte | 1 |
|
|
short | 2 |
|
|
char | 2 |
|
|
int | 4 |
|
|
long | 8 |
|
|
float | 4 |
|
|
double | 8 |
|
|
String (UTF) | variabile |
Note:
- Per le stringhe si usano più spesso writeUTF/readUTF (si scrive la lunghezza della stringa e poi i byte in UTF-8).
- Se vuoi scrivere un array, scrivine prima la lunghezza e poi gli elementi uno per uno.
4. Esempio avanzato: salviamo array di primitivi
Scrittura di un array
int[] scores = {100, 200, 300, 400, 500};
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream("scores.bin"))) {
dos.writeInt(scores.length); // Prima scriviamo la lunghezza dell’array
for (int score : scores) {
dos.writeInt(score); // Poi ogni elemento
}
}
Lettura di un array
try (DataInputStream dis = new DataInputStream(
new FileInputStream("scores.bin"))) {
int length = dis.readInt(); // Leggiamo la lunghezza
int[] scores = new int[length];
for (int i = 0; i < length; i++) {
scores[i] = dis.readInt(); // Leggiamo gli elementi
}
// Stampiamo l’array
for (int score : scores) {
System.out.println(score);
}
}
Perché prima la lunghezza?
Perché in fase di lettura non sappiamo quanti numeri sono stati scritti. Scrivendo la lunghezza all’inizio rendiamo il formato del file autoesplicativo.
5. Dettagli e particolarità importanti
Quando conviene usare DataInputStream/DataOutputStream?
- Quando devi salvare/caricare dati strutturati composti da primitivi.
- Per lo scambio di dati binari tra programmi Java (o anche tra linguaggi diversi, se conosci il formato).
- Quando contano compattezza e velocità (per esempio log, risultati di calcolo, grandi array di numeri).
Quando non conviene:
- Se serve un formato leggibile dall’uomo (CSV, JSON, XML) — usa formati testuali.
- Per oggetti complessi con annidamenti — meglio usare la serializzazione con ObjectOutputStream/ObjectInputStream (argomento a parte).
Buffering
DataOutputStream e DataInputStream non effettuano buffering di per sé. Se vuoi aumentare le prestazioni con file grandi, avvolgili in BufferedOutputStream/BufferedInputStream:
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
// ...
}
Codifica delle stringhe
I metodi writeUTF/readUTF usano un formato speciale: prima si scrive la lunghezza della stringa (in byte), poi il contenuto in UTF-8. Da non confondere con la semplice scrittura di un array di byte!
Eccezioni
Le operazioni di lettura/scrittura possono lanciare IOException se il file è non accessibile, danneggiato o termina in anticipo. Se provi a leggere più di quanto sia stato scritto, spesso viene lanciata EOFException. Usa sempre la costruzione try-with-resources oppure gestisci l’eccezione con try-catch.
Ordine di lettura e scrittura
L’errore più comune — la mancata corrispondenza tra ordine di scrittura e ordine di lettura. Se hai scritto: int, double, boolean, ma leggi come double, int, boolean, otterrai dati errati o un’eccezione.
6. Errori tipici
Errore n. 1: ordine di scrittura e lettura non coerente. Se cambi l’ordine dei metodi, i dati verranno letti in modo errato oppure verrà lanciata un’eccezione. Per esempio, se prima hai scritto una stringa e poi un numero, ma in lettura provi prima a leggere un numero — otterrai un errore di formato.
Errore n. 2: dimenticare di scrivere la lunghezza dell’array. Se scrivi un array di primitivi ma non ne scrivi la lunghezza, in lettura non saprai quanti elementi leggere. Questo porta o a un errore, oppure a «dati in più» alla fine.
Errore n. 3: tentare di leggere oltre la fine del file. Se leggi più dati di quanti ne siano stati scritti, otterrai EOFException (end of file).
Errore n. 4: usare DataInputStream/DataOutputStream per file di testo. Queste classi non sono pensate per leggere normali file di testo creati, per esempio, nel Blocco note. Se provi a leggere un tale file con readInt() — otterrai dati privi di senso o un errore.
Errore n. 5: non chiudere lo stream. Se non usi try-with-resources o non chiudi gli stream manualmente, il file può restare non disponibile per altri programmi o non essere salvato completamente.
GO TO FULL VERSION