CodeGym /Kurse /JAVA 25 SELF /DataInputStream, DataOutputStream: Arbeit mit primitiven ...

DataInputStream, DataOutputStream: Arbeit mit primitiven Typen

JAVA 25 SELF
Level 36 , Lektion 3
Verfügbar

1. Wozu braucht man DataInputStream und DataOutputStream?

Wenn Sie mit Dateien arbeiten, muss man manchmal nicht nur Text, sondern strukturierte Daten speichern: Zahlen, boolesche Werte, Arrays primitiver Typen. Stellen Sie sich zum Beispiel vor, Sie schreiben ein simples Spiel und möchten den Fortschritt des Nutzers speichern: die Anzahl der Punkte (int), das aktuelle Level (int), die Spielzeit (double), den Gewinnerstatus (boolean). Natürlich kann man das im Textformat ablegen:

12345
5
67.5
true

Aber das ist unpraktisch und unsicher: Zeichenketten müssen geparst werden, das Format kann sich ändern, und Zahlen brauchen mehr Platz.

Die Idee besteht darin, die Daten „roh“ (binär) in die Datei zu schreiben, ohne sie in Text umzuwandeln. Dafür gibt es in Java zwei hervorragende Klassen:

  • DataOutputStream — kann primitive Typen in einen Stream schreiben.
  • DataInputStream — kann primitive Typen aus einem Stream lesen.

Sie arbeiten auf gewöhnlichen Byte-Streams (OutputStream/InputStream). Das sind also „Wrapper“, die nicht einfach nur Bytes schreiben, sondern wissen, wie man aus diesen Bytes int, double, boolean und sogar String zusammensetzt.

Wie funktioniert das?

Einen normalen FileOutputStream kann man sich wie ein Fließband vorstellen, auf das Sie die Bytes manuell nacheinander legen. Wenn Sie eine Ganzzahl oder einen String schreiben möchten, müssen Sie selbst im Blick behalten, wie viele Bytes jedes Element belegt.

DataOutputStream macht das Leben leichter: Er wirkt wie ein Roboter an diesem Fließband. Sie sagen ihm „schreibe eine Zahl“ oder „schreibe einen String“, und er packt die Daten selbst in die benötigte Anzahl von Bytes und schickt sie auf die Platte. Am anderen Ende des Fließbands steht der gleiche Roboter — DataInputStream — der diese Bytes wieder zu den ursprünglichen Objekten zusammensetzen kann.

Warum ist das praktisch? Weil Sie nicht über die Anzahl der Bytes für int, double oder boolean nachdenken müssen. Die Daten werden kompakt gespeichert, schnell gelesen und geschrieben, und es drohen weder Parserfehler noch Formatprobleme.

2. Beispiel: Schreiben und Lesen von Primitiven

Nehmen wir an, wir möchten die Ergebnisse unseres (gedachten) Spiels speichern: den Namen des Spielers (String), die Anzahl der Punkte (int), die Bestzeit (double), ob der Spieler gewonnen hat (boolean).

Daten in eine Datei schreiben

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);    // Schreiben der Zeichenkette (UTF-8)
            dos.writeInt(score);         // int schreiben (4 Byte)
            dos.writeDouble(recordTime); // double schreiben (8 Byte)
            dos.writeBoolean(isWinner);  // boolean schreiben (1 Byte)
            System.out.println("Daten wurden erfolgreich in die Datei geschrieben!");
        } catch (IOException e) {
            System.out.println("Schreibfehler: " + e.getMessage());
        }
    }
}
  • writeUTF(String) — schreibt eine Zeichenkette im UTF-8-Format (mit Länge am Anfang).
  • writeInt(int) — schreibt 4 Byte.
  • writeDouble(double) — schreibt 8 Byte.
  • writeBoolean(boolean) — schreibt 1 Byte (1 oder 0).
  • Alle Methoden verpacken die Daten automatisch im richtigen Format.

Daten aus einer Datei lesen

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();      // Zeichenkette lesen
            int score = dis.readInt();              // int lesen
            double recordTime = dis.readDouble();   // double lesen
            boolean isWinner = dis.readBoolean();   // boolean lesen

            System.out.println("Spielername: " + playerName);
            System.out.println("Punkte: " + score);
            System.out.println("Zeit: " + recordTime);
            System.out.println("Gewonnen: " + isWinner);
        } catch (IOException e) {
            System.out.println("Lesefehler: " + e.getMessage());
        }
    }
}

Wichtiger Punkt:
Die Lesereihenfolge muss der Schreibrreihenfolge entsprechen! Wenn Sie zuerst einen String, dann int und dann double geschrieben haben — müssen Sie in genau derselben Reihenfolge lesen. Andernfalls erhalten Sie einen Fehler oder Datenmüll.

3. Welche Typen werden unterstützt?

DataOutputStream und DataInputStream unterstützen alle grundlegenden primitiven Java-Typen:

Schreibmethode Lesemethode Datentyp Größe (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) variabel

Hinweise:
- Für Strings verwendet man meist writeUTF/readUTF (es werden die Länge der Zeichenkette und anschließend die Bytes in UTF-8 geschrieben).
- Wenn Sie ein Array schreiben möchten, schreiben Sie zuerst seine Länge und danach die Elemente einzeln.

4. Fortgeschrittenes Beispiel: Arrays primitiver Typen speichern

Array schreiben

int[] scores = {100, 200, 300, 400, 500};
try (DataOutputStream dos = new DataOutputStream(
        new FileOutputStream("scores.bin"))) {
    dos.writeInt(scores.length); // Zuerst die Länge des Arrays schreiben
    for (int score : scores) {
        dos.writeInt(score);     // Dann jedes Element
    }
}

Array lesen

try (DataInputStream dis = new DataInputStream(
        new FileInputStream("scores.bin"))) {
    int length = dis.readInt();      // Länge lesen
    int[] scores = new int[length];
    for (int i = 0; i < length; i++) {
        scores[i] = dis.readInt();   // Elemente lesen
    }
    // Array ausgeben
    for (int score : scores) {
        System.out.println(score);
    }
}

Warum zuerst die Länge?
Weil wir beim Lesen nicht wissen, wie viele Zahlen geschrieben wurden. Indem wir die Länge am Anfang schreiben, machen wir das Dateiformat selbstdokumentierend.

5. Wichtige Details und Besonderheiten

Wann sollte man DataInputStream/DataOutputStream verwenden?

  • Wenn strukturierte Daten gespeichert/geladen werden sollen, die aus primitiven Typen bestehen.
  • Für den Austausch binärer Daten zwischen Java-Programmen (oder sogar verschiedenen Sprachen, wenn Sie das Format kennen).
  • Wenn Kompaktheit und Geschwindigkeit wichtig sind (z. B. Logs, Rechenergebnisse, große Zahlenarrays).

Wann nicht:

  • Wenn ein menschenlesbares Format benötigt wird (CSV, JSON, XML) — verwenden Sie Textformate.
  • Für komplexe, verschachtelte Objekte — nutzen Sie besser die Serialisierung über ObjectOutputStream/ObjectInputStream (eigenes Thema).

Pufferung

DataOutputStream und DataInputStream puffern die Daten nicht von selbst. Wenn Sie die Leistung bei großen Dateien verbessern möchten, umhüllen Sie sie mit BufferedOutputStream/BufferedInputStream:

try (DataOutputStream dos = new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    // ...
}

Zeichenkodierung von Strings

Die Methoden writeUTF/readUTF verwenden ein spezielles Format: Zuerst wird die Länge der Zeichenkette (in Bytes) geschrieben, danach der Inhalt in UTF-8. Verwechseln Sie das nicht mit dem einfachen Schreiben eines Byte-Arrays!

Ausnahmen

Lese-/Schreiboperationen können eine IOException werfen, wenn die Datei nicht verfügbar ist, beschädigt ist oder früher als erwartet endet. Beim Versuch, mehr zu lesen als geschrieben wurde, tritt häufig eine EOFException auf. Verwenden Sie immer try-with-resources oder behandeln Sie die Ausnahme mit einem try-catch-Block.

Reihenfolge von Lesen und Schreiben

Der häufigste Fehler — die Lesereihenfolge passt nicht zur Schreibrreihenfolge. Wenn Sie geschrieben haben: int, double, boolean, aber lesen als double, int, boolean, erhalten Sie falsche Daten oder eine Ausnahme.

6. Typische Fehler

Fehler Nr. 1: Verletzung der Schreib-/Lesereihenfolge. Wenn Sie die Reihenfolge der Methoden ändern, werden die Daten falsch gelesen oder es wird eine Ausnahme geworfen. Wenn Sie zum Beispiel zuerst einen String und dann eine Zahl schreiben, beim Lesen aber zuerst eine Zahl lesen wollen — erhalten Sie einen Formatfehler.

Fehler Nr. 2: Die Array-Länge wurde nicht geschrieben. Wenn Sie ein Array primitiver Typen schreiben, aber dessen Länge nicht mitschreiben, wissen Sie beim Lesen nicht, wie viele Elemente zu lesen sind. Das führt entweder zu einem Fehler oder zu „überflüssigen“ Daten am Ende.

Fehler Nr. 3: Lesen über das Dateiende hinaus. Wenn Sie mehr Daten lesen, als geschrieben wurden, erhalten Sie eine EOFException (end of file).

Fehler Nr. 4: Verwendung von DataInputStream/DataOutputStream für Textdateien. Diese Klassen sind nicht dazu gedacht, gewöhnliche Textdateien zu lesen, die zum Beispiel in Notepad erstellt wurden. Wenn Sie versuchen, eine solche Datei mit readInt() zu lesen — erhalten Sie sinnlose Daten oder einen Fehler.

Fehler Nr. 5: Der Stream wurde nicht geschlossen. Wenn Sie try-with-resources nicht verwenden oder Streams nicht manuell schließen, kann die Datei für andere Programme gesperrt bleiben oder nicht vollständig gespeichert werden.

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