CodeGym /Corsi /JAVA 25 SELF /DataInputStream, DataOutputStream: lavoro con i tipi prim...

DataInputStream, DataOutputStream: lavoro con i tipi primitivi

JAVA 25 SELF
Livello 36 , Lezione 3
Disponibile

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)
writeBoolean(boolean)
readBoolean()
boolean 1
writeByte(int)
readByte()
byte 1
writeShort(int)
readShort()
short 2
writeChar(int)
readChar()
char 2
writeInt(int)
readInt()
int 4
writeLong(long)
readLong()
long 8
writeFloat(float)
readFloat()
float 4
writeDouble(double)
readDouble()
double 8
writeUTF(String)
readUTF()
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.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION