W3docs

Stream DataInput e DataOutput in Java

Leggi e scrivi tipi primitivi Java in formato binario portabile con DataInputStream e DataOutputStream.

Finora in questa parte: byte (grezzi o con buffer) per dati binari arbitrari, caratteri per il testo. Esiste un terzo caso d'uso che i capitoli precedenti non coprono — scrivere un int, double o boolean Java su un file e rileggerlo come lo stesso tipo, in un formato con cui un'altra JVM (che gira su un OS diverso, con un ordine dei byte predefinito diverso) sarà d'accordo.

È per questo che esistono DataInputStream e DataOutputStream. Sono decoratori che si appoggiano sopra qualsiasi stream di byte e aggiungono metodi di lettura/scrittura tipizzati: writeInt, writeDouble, writeUTF, readInt, readDouble, readUTF. Il formato binario è documentato, fisso, big-endian e portabile su ogni JVM mai distribuita.

Ciò che scrivi è ciò che leggi

DataOutputStream espone un metodo per ogni tipo primitivo:

void writeBoolean(boolean v);    //  1 byte (0 or 1)
void writeByte(int v);            //  1 byte (low 8 bits)
void writeShort(int v);           //  2 bytes, big-endian
void writeChar(int v);            //  2 bytes, big-endian (UTF-16 code unit)
void writeInt(int v);             //  4 bytes, big-endian
void writeLong(long v);           //  8 bytes, big-endian
void writeFloat(float v);         //  4 bytes, IEEE 754
void writeDouble(double v);       //  8 bytes, IEEE 754
void writeUTF(String s);          //  modified UTF-8 with a 2-byte length prefix

DataInputStream dispone dei corrispondenti readInt, readLong, readUTF e così via. Il contratto è simmetrico: scrivi un int con writeInt, rileggilo con readInt e ottieni lo stesso numero, ogni volta, su ogni JVM, su ogni sistema operativo.

Tre cose da interiorizzare:

  1. Il formato non ha separatori di campo. Un file con writeInt(42); writeUTF("alice"); writeDouble(3.14) è composto da 4 + 2 + 5 + 8 = 19 byte disposti senza marcatori tra loro. Devi leggere nello stesso ordine con gli stessi tipi. Non esiste schema, né auto-descrizione, né recupero se indovini male.

  2. writeUTF è UTF-8 modificato. Il prefisso è una lunghezza a 16 bit senza segno (quindi massimo 65.535 byte per string), e U+0000 è codificato come due byte (0xC0 0x80) invece del singolo byte standard. Il formato è incompatibile con l'UTF-8 semplice — non puoi leggere una string writeUTF con un Reader. Usalo solo quando entrambi i lati sono Java.

  3. Big-endian, sempre. L'ordine nativo dei byte della macchina varia (x86 è little-endian, i protocolli di rete sono big-endian) ma DataOutputStream scrive sempre big-endian in modo incondizionato. Questo è ciò che rende il formato portabile. Se hai bisogno di little-endian per un protocollo che non controlli, usa invece java.nio.ByteBuffer — ha un ordine dei byte configurabile.

Quando usare gli stream di dati

Due casi:

  • Controlli entrambi i lati e vuoi un formato binario semplice, compatto e portabile tra linguaggi. Un "file di salvataggio" per un piccolo gioco Java, un file di fixture per un unit test, una cache che non deve sopravvivere alla versione della JVM. Il formato è semplice da scrivere e analizzare; non hai bisogno di una libreria di serializzazione.
  • Stai leggendo un formato di file che usa il layout degli stream di dati Java. I file di classe (.class), i record formattati con RandomAccessFile, alcuni file di indice .jar. Tutti questi sono stati scritti con DataOutputStream perché il JDK costruisce il formato stesso.

Quando hai bisogno di interoperabilità tra linguaggi (Python, Go, JS), usa JSON, Protocol Buffers o MessagePack. Quando hai bisogno di versioning ed evoluzione dello schema, ObjectOutputStream è più vicino — ma è più pesante e ha le sue insidie.

La regola di fine file

Mentre InputStream.read() restituisce -1 alla fine dello stream, DataInputStream.readInt() (e simili) lancia EOFException. Non esiste un sentinel in-band — un int legale può essere qualsiasi valore a 32 bit, incluso -1, quindi l'unico modo per segnalare la fine dello stream è l'eccezione.

try (DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)))) {
  try {
    while (true) {
      int x = in.readInt();
      process(x);
    }
  } catch (EOFException e) {
    // normal end of stream
  }
}

Quel try/catch per la terminazione normale è la forma idiomatica. È insolito per il JDK rendere un segnale di controllo del flusso un'eccezione, ma l'API di lettura tipizzata non ha altra scelta — non c'è nessun valore da restituire che non sia anche un int valido.

Per i file in cui controlli il formato, il pattern migliore è scrivere un prefisso di lunghezza all'inizio:

out.writeInt(n);
for (int i = 0; i < n; i++) out.writeInt(values[i]);

Poi il lato di lettura esegue il ciclo n volte e non deve mai catturare EOFException per il controllo del flusso.

Usa il buffer prima di decorare

DataInputStream non esegue il buffering. Ogni readInt diventa una serie di chiamate read() sullo stream sottostante. Se quello stream è un FileInputStream, ogni readInt è quattro syscall. Avvolgi sempre con BufferedInputStream prima:

// Right
DataInputStream  in  = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)));
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(path)));

Questo è lo stack standard a tre livelli: file → buffered → data. Lo stesso ordine si applica alla scrittura. Ometti il buffer e paghi il costo di syscall-per-byte del capitolo sugli stream con buffer, moltiplicato per il numero di byte per primitivo.

Un esempio pratico: un piccolo formato di record binario

Il programma seguente definisce un record binario minimale — un int id, un nome UTF, un double score, un boolean active — e scrive alcuni record su un file temporaneo con DataOutputStream. Li rilegge con DataInputStream usando sia il pattern count-prefix che il pattern EOFException, e infine mostra la modalità di errore di mancata corrispondenza del formato in cui il lettore e lo scrittore non sono d'accordo sui tipi di campo.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • La dimensione del file è risultata esattamente quella che avresti previsto sommando le larghezze tipizzate: 4 (conteggio) + per record (4 + UTF con prefisso di lunghezza + 8 + 1). Nessun padding, nessun separatore. Un file data-stream è semplicemente i byte disposti, nient'altro.
  • Entrambi i pattern di lettura hanno prodotto gli stessi tre record. Il pattern count-prefix è quello migliore quando stai progettando il formato; il pattern EOFException è quello a cui ricorri quando non puoi modificare lo scrittore e il formato è aperto.
  • Il blocco di mancata corrispondenza del formato ha scritto due int e letto un long. I byte su disco (00 00 00 2A 00 00 00 63) erano validi per entrambe le interpretazioni — DataInputStream non ha modo di distinguere. Le due interpretazioni sono mutuamente coerenti byte per byte e mutuamente errate a livello semantico. Questo è il costo di un formato binario senza schema: la disciplina al confine è l'unica protezione.
  • Ogni stream era incapsulato Files.newInputStreamBufferedInputStreamDataInputStream (e lo stesso sul lato di scrittura). Ometti il buffer e readInt diventa quattro syscall; il livello data-stream è puramente conversione di formato e non aggiunge alcun buffering proprio.
  • writeUTF è stato usato per il nome. Il formato va bene per la comunicazione inter-Java ed è inutile per qualsiasi altra cosa — non sceglierlo per un file di configurazione che un giorno potresti leggere in Python. Per "solo Java e voglio che sia piccolo," è lo strumento giusto; per "qualcun altro potrebbe leggerlo," usa JSON o Protobuf.

Cosa c'è dopo

Gli stream di dati gestiscono un primitivo alla volta e richiedono che il lettore conosca il formato. Il prossimo capitolo, Java PrintWriter, torna al lato dei caratteri e copre il decoratore Writer che aggiunge print, println e printf — l'API che usi su System.out dal capitolo 1, finalmente come file-writer quale è sempre stato.

Esercitazione

Pratica
Un file è stato scritto da `DataOutputStream` su un server Linux x86 (ordine dei byte nativo little-endian) con `out.writeInt(1)`. Cosa restituisce `DataInputStream.readInt()` su un laptop Windows ARM che legge lo stesso file?
Un file è stato scritto da `DataOutputStream` su un server Linux x86 (ordine dei byte nativo little-endian) con `out.writeInt(1)`. Cosa restituisce `DataInputStream.readInt()` su un laptop Windows ARM che legge lo stesso file?
Was this page helpful?