W3docs

Java DataInput und DataOutput Streams

Java-Primitive im portablen Binärformat mit DataInputStream und DataOutputStream lesen und schreiben.

Bisher in diesem Teil: Bytes (roh oder gepuffert) für beliebige Binärdaten, Zeichen für Text. Es gibt einen dritten Anwendungsfall, den die bisherigen Kapitel nicht abdecken — einen Java-int, double oder boolean in eine Datei zu schreiben und ihn als denselben Typ zurückzulesen, in einem Format, auf das sich eine andere JVM (auf einem anderen Betriebssystem mit einer anderen Standard-Bytereihenfolge) einigen kann.

Genau dafür existieren DataInputStream und DataOutputStream. Sie sind Dekoratoren, die auf einem beliebigen Byte-Stream aufsitzen und typisierte Lese-/Schreibmethoden hinzufügen: writeInt, writeDouble, writeUTF, readInt, readDouble, readUTF. Das Binärformat ist dokumentiert, festgelegt, big-endian und portabel über alle je ausgelieferten JVMs.

Was geschrieben wird, wird gelesen

DataOutputStream stellt eine Methode pro primitivem Typ bereit:

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 hat die entsprechenden Methoden readInt, readLong, readUTF und so weiter. Der Vertrag ist symmetrisch: Schreibe einen int mit writeInt, lese ihn mit readInt zurück und erhalte dieselbe Zahl — jedes Mal, auf jeder JVM, auf jedem Betriebssystem.

Drei Dinge sollte man verinnerlichen:

  1. Das Format hat keine Feldtrennzeichen. Eine Datei mit writeInt(42); writeUTF("alice"); writeDouble(3.14) besteht aus 4 + 2 + 5 + 8 = 19 Bytes, die ohne Markierungen dazwischen geschrieben werden. Man muss in derselben Reihenfolge mit denselben Typen lesen. Es gibt kein Schema, keine Selbstbeschreibung und keine Wiederherstellung, wenn man falsch liegt.

  2. writeUTF ist modifiziertes UTF-8. Das Präfix ist eine vorzeichenlose 16-Bit-Länge (also maximal 65.535 Bytes pro String), und U+0000 wird als zwei Bytes (0xC0 0x80) statt als ein Byte kodiert. Das Format ist inkompatibel mit einfachem UTF-8 — ein writeUTF-String kann nicht mit einem Reader gelesen werden. Verwende es nur, wenn beide Seiten Java nutzen.

  3. Big-endian, immer. Die native Bytereihenfolge der Maschine variiert (x86 ist little-endian, Netzwerkprotokolle sind big-endian), aber DataOutputStream schreibt bedingungslos big-endian. Das macht das Format portabel. Wenn man little-endian für ein Protokoll benötigt, das man nicht kontrolliert, sollte man stattdessen java.nio.ByteBuffer verwenden — er hat eine einstellbare Bytereihenfolge.

Wann man auf Data Streams zurückgreift

Zwei Fälle:

  • Man kontrolliert beide Seiten und möchte ein einfaches, kompaktes, sprachübergreifend portables Binärformat. Eine Speicherdatei für ein kleines Java-Spiel, eine Fixture-Datei für einen Unit-Test, ein Cache, der nicht länger als die JVM-Version leben muss. Das Format ist einfach zu schreiben und zu parsen; man bindet keine Serialisierungsbibliothek ein.
  • Man liest ein Dateiformat, das zufällig das Java-Data-Stream-Layout verwendet. Klassendateien (.class), RandomAccessFile-formatierte Einträge, einige .jar-Indexdateien. Diese wurden alle mit DataOutputStream geschrieben, weil das JDK das Format selbst erstellt.

Wenn man sprachübergreifende Interoperabilität (Python, Go, JS) benötigt, greift man zu JSON, Protocol Buffers oder MessagePack. Wenn man Versionierung und Schema-Evolution braucht, ist ObjectOutputStream die nähere Option — aber schwerer und mit eigenen Tücken.

Die Ende-der-Datei-Regel

Während InputStream.read() am Ende des Streams -1 zurückgibt, wirft DataInputStream.readInt() (und Verwandtes) eine EOFException. Es gibt keinen internen Sentinel — ein zulässiger int kann ein beliebiger 32-Bit-Wert sein, einschließlich -1, sodass das Ende des Streams nur durch die Ausnahme signalisiert werden kann.

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
  }
}

Dieses try/catch für die normale Beendigung ist die idiomatische Form. Es ist ungewöhnlich für das JDK, ein Kontrollfluss-Signal aus einer Ausnahme zu machen, aber die typisierte Lese-API hat keine andere Möglichkeit — es gibt keinen Rückgabewert, der nicht auch ein gültiger int wäre.

Für Dateien, bei denen man das Format kontrolliert, ist das bessere Muster, am Anfang ein Längenpräfix zu schreiben:

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

Dann schleift die Leseseite n-mal und muss EOFException niemals als Kontrollfluss abfangen.

Erst puffern, dann dekorieren

DataInputStream puffert nicht. Jedes readInt wird zu einer Reihe von read()-Aufrufen auf dem darunterliegenden Stream. Wenn dieser Stream ein FileInputStream ist, sind das vier Syscalls pro readInt. Immer zuerst mit BufferedInputStream umwickeln:

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

Das ist der übliche dreistufige Stack: Datei → gepuffert → Daten. Dieselbe Reihenfolge gilt fürs Schreiben. Ohne Puffer zahlt man die Syscall-pro-Byte-Kosten aus dem Buffered-Streams-Kapitel, multipliziert mit der Anzahl der Bytes pro Primitiv.

Ein Praxisbeispiel: ein minimales Binärdatensatzformat

Das folgende Programm definiert einen minimalen Binärdatensatz — eine int-id, einen UTF-Namen, einen double-Score, ein boolean active — und schreibt einige Datensätze mit DataOutputStream in eine temporäre Datei. Es liest sie mit DataInputStream zurück, sowohl mit dem Zählpräfix-Muster als auch mit dem EOFException-Muster, und zeigt schließlich den Format-Mismatch-Fehlerfall, bei dem Leser und Schreiber in den Feldtypen nicht übereinstimmen.

java— editable, runs on the server

Was man aus dem Lauf mitnehmen sollte:

  • Die Dateigröße entsprach genau den Bytes, die man durch Addition der typisierten Breiten vorhersagen würde: 4 (Zähler) + pro Datensatz (4 + längen-präfixiertes UTF + 8 + 1). Keine Füllbytes, keine Trennzeichen. Eine Data-Stream-Datei sind die geschriebenen Bytes — nichts weiter.
  • Beide Lesemuster lieferten dieselben drei Datensätze. Das Zählpräfix-Muster ist das bessere, wenn man das Format selbst gestaltet; das EOFException-Muster ist der Rückgriff, wenn man den Schreiber nicht ändern kann und das Format offen ist.
  • Der Format-Mismatch-Block schrieb zwei ints und las einen long. Die Bytes auf der Festplatte (00 00 00 2A 00 00 00 63) waren für beide Interpretationen gültig — DataInputStream kann nicht unterscheiden. Die zwei Interpretationen sind byte-weise konsistent und auf semantischer Ebene gegenseitig falsch. Das ist der Preis eines schemafreien Binärformats: Disziplin an der Grenzstelle ist der einzige Schutz.
  • Jeder Stream war als Files.newInputStreamBufferedInputStreamDataInputStream geschachtelt (und ebenso auf der Schreibseite). Ohne Puffer werden aus readInt vier Syscalls; die Data-Stream-Schicht ist reine Formatkonvertierung und fügt kein eigenes Buffering hinzu.
  • writeUTF wurde für den Namen verwendet. Das Format ist für Java-zu-Java-Kommunikation geeignet und für alles andere unbrauchbar — nicht wählen für eine Konfigurationsdatei, die man vielleicht eines Tages in Python lesen möchte. Für "nur Java und ich möchte es klein halten" ist es das richtige Werkzeug; für "jemand anderes könnte das lesen" — JSON oder Protobuf.

Was kommt als nächstes

Data Streams verarbeiten jeweils ein Primitiv und verlangen vom Leser Formatkenntnis. Das nächste Kapitel, Java PrintWriter, kehrt zur Zeichenseite zurück und behandelt den Writer-Dekorator, der print, println und printf hinzufügt — die API, die man seit Kapitel 1 auf System.out verwendet hat, endlich als Datei-Schreiber, der sie immer war.

Übungen

Übung
Eine Datei wurde von `DataOutputStream` auf einem Linux-x86-Server (little-endian native Bytereihenfolge) mit `out.writeInt(1)` geschrieben. Was gibt `DataInputStream.readInt()` auf einem Windows-ARM-Laptop zurück, der dieselbe Datei liest?
Eine Datei wurde von `DataOutputStream` auf einem Linux-x86-Server (little-endian native Bytereihenfolge) mit `out.writeInt(1)` geschrieben. Was gibt `DataInputStream.readInt()` auf einem Windows-ARM-Laptop zurück, der dieselbe Datei liest?
Was this page helpful?