09.05 Beliebige Daten lesen und schreiben

Im letzten Kapitel haben Sie gelernt, wie Sie Text in einer Textdatei speichern können. Aber natürlich gibt es noch viel mehr Arten von Daten als Texte. Bilder und Musik liegen bspw. nicht als geschriebener Text, sondern in binärer Form vor. In diesem Fall greifen Sie auf java.io.FileInputStream anstelle von java.io.FileReader und java.io.FileOutputStream anstelle von java.io.FileWriter zu. Der Hauptunterschied liegt darin, dass Bytes anstelle von Zeichen verarbeitet werden. Die Handhabung ist hingegen weitestgehend identisch – Sie können sogar mittels java.io.BufferedInputStream und java.io.BufferedOutputStream die Streams wie im vorhergehenden Kapitel puffern.

Text „binär“ speichern

Auch Text besteht in der Computerwelt genau genommen aus Nullen und Einsen und somit Bits und Bytes. Deshalb können Sie mit Streams genauso Text schreiben und Lesen wie mit Readern und Writern. Sie müssen die Buchstaben lediglich in Bytes konvertieren. Sehen Sie sich hierzu ein einfaches Einstiegsbeispiel an:

File f = new File("C:/test.txt");
FileOutputStream fos = null;
try {
  fos = new FileOutputStream(f);
  fos.write("Test Ausgabe".getBytes());
}
catch (IOException e) {
  e.printStackTrace();
}
finally {
  if (fos != null) try { fos.close(); } catch (IOException e) {}
}

FileInputStream fis = null;
try {
  fis = new FileInputStream(f);
  byte[] b = new byte[(int)f.length()];
  fis.read(b);
  System.out.println(new String(b));
}
catch (IOException e) {
  e.printStackTrace();
}
finally {
  if (fis != null) try { fis.close(); } catch (IOException e) {}
}

Sie sehen, dass sich der Einsatz von Streams nur sehr geringfügig vom bereits Gelerntem unterscheidet. Aber das ist natürlich nicht der häufigste Einsatzort für Streams.

Daten binär speichern

Die bekanntesten binär verfügbaren Dateien sind wohl Musikstücke (z. B. im Dateiformat mp3), Bilder (bspw. jpeg, png, bmp, tiff), PDF-Dateien für den Adobe Acrobat Reader und Programme (exe (Windows)). Der Inhalt dieser Dateien liegt in Bits und Bytes vor und kann von entsprechenden Programmen (Bildbearbeitungssoftware, Musikplayer, Acrobat Reader) bzw. direkt vom Betriebssystem interpretiert und wiedergegeben werden. Dazu muss die Datei aber genau so aufgebaut werden, wie es die verarbeitende Schnittstelle erwartet. Dieser Aufbau stellt das so genannte (Datei-) Format der Daten dar. Dieses spezifiziert, welche Informationen an welcher Stelle stehen müssen. So kann es z. B. vorgegeben sein, dass an den Anfang der Datei ein Header mit Metainformationen gestellt wird. Erst nach diesem Header folgen die Daten in einer ganz bestimmten Reihenfolge und Form.

Ein solches gängiges Dateiformat an dieser Stelle zu lesen und schreiben würde den Rahmen sprengen, zumal es für viele Formate bereits eine Standardimplementierung in Java gibt. Deshalb beschränken wir uns in diesem Kapitel auf eine einfachere Anforderung: Einen großen Long-Wert jenseits der Integer-Grenze möglichst speichersparend in eine Datei zu schreiben.

Angenommen der größtmögliche Long-Wert würde als String in eine Datei geschrieben werden:

FileOutputStream fos = null;
try {
  fos = new FileOutputStream("C:/test.txt");
  fos.write(String.valueOf(Long.MAX_VALUE).getBytes());
}
catch (IOException e) {
  e.printStackTrace();
}
finally {
  if (fos != null) try { fos.close(); } catch (IOException e) {}
}

Als Resultat bekommen Sie eine Textdatei, die 19 Byte groß ist, und folgendes beinhaltet:

9223372036854775807

Falls Sie sich noch an das Kapitel 02.03 Primitive Datentypen erinnern können, ist ein long aber nur acht byte groß – und keine 19. Dies liegt daran, dass bei der soeben gezeigten Variante für jede einzelne Ziffer ein separates Byte in die Datei geschrieben werden muss. In Wirklichkeit liegen die Ziffern aber in binärer Form direkt als eine große Zahl in mehreren aufeinanderfolgenden Bytes vor. Wenn Sie den Long mit Hilfe einer einfachen Konvertierungsmethode vor dem Speichervorgang in ein entsprechendes byte-Array umwandeln, und dieses dann abspeichern, können Sie den Platzbedarf der Datei auf der Festplatte auf acht Byte reduzieren.

FileOutputStream fos = null;
try {
  fos = new FileOutputStream("C:/test.txt");
  fos.write(convert(Long.MAX_VALUE));
}
catch (IOException e) {
  e.printStackTrace();
}
finally {
  if (fos != null) try { fos.close(); } catch (IOException e) {}
}

...

public byte[] convert(long l) {

  byte[] b = new byte[8];
  for (int i = 0; i < b.length; i++) {
    b[i] = (byte)((l & (255L << (56 - 8 * i))) >> (56 - 8 * i));
  }
  return b;
}

Allerdings sollten Sie nun nicht mehr erwarten, dass die Textdatei beim Öffnen mit einem gewöhnlichen Editor noch Rückschlüsse auf den eigentlichen Inhalt oder gar Manipulationsmöglichkeiten zulässt. Der Inhalt wird vermutlich so oder so ähnlich aussehen:

ÿÿÿÿÿÿÿ

Dennoch können Sie – indem Sie die Konvertierung nach dem Lesen der Datei umkehren – die gespeicherte Zahl wieder in Ihrem Java-Programm auslesen und verwenden:

FileInputStream fis = null;
try {
  fis = new FileInputStream("C:/test.txt");
  byte[] b = new byte[8];
  fis.read(b);
  System.out.println(convert(b));
}
catch (IOException e) {
  e.printStackTrace();
}
finally {
  if (fis != null) try { fis.close(); } catch (IOException e) {}
}

...

public long convert(byte[] bytes) {

  long l = 0;
  for (int i = 0; i < bytes.length; i++) {
    l |= (long)((bytes[i] & 0xFFL) << (56 - 8 * i));
  }
  return l;
}

Dies kann auch jedes andere Programm. Jedoch nur, wenn es die Konvertierung (das Dateiformat) kennt.

Basistypen schreiben

Selbstverständlich müssen Sie sich in Java nicht so wie eben verbiegen, nur um effizient einen Datentypen wie den long zu schreiben. Java bietet Ihnen hier den java.io.DataOutputStream und den java.io.DataInputStream. Mit diesen Streams können Sie häufig verwendete Datendarstellungen lesen und schreiben. Bspw. schreibt/liest write/readLong einen long, write/readByte ein byte und read/writeUTF einen String in einem modifiziertem UTF-8 Format.

Um einen DataInput/DataOutputStream zu erzeugen, übergeben Sie diesem im Konstruktor einfach den gewünschten OutputStream oder InputStream. Als Beispiel schreiben wir N Strings mit vorangestellter Anzahl der Strings in eine Datei und lesen diese später wieder aus.

String[] strings = {
  "ich bin ein String-Array",
  "und bestehe aus mehreren Strings",
  "die alle in eine Datei geschrieben werden"
};
DataOutputStream dos = null;
FileOutputStream fos = null;
try {
  fos = new FileOutputStream("strings.bin");
  dos = new DataOutputStream(fos);
  dos.writeInt(strings.length);
  for (String str : strings) {
    dos.writeUTF(str);
  }
}
catch (FileNotFoundException e) {
  e.printStackTrace();
}
catch (IOException e) {
  e.printStackTrace();
}
finally {
  if (dos != null) try { dos.close(); } catch (IOException e) {}
  if (fos != null) try { fos.close(); } catch (IOException e) {}
}
DataInputStream dis = null;
FileInputStream fis = null;
try {
  fis = new FileInputStream("strings.bin");
  dis = new DataInputStream(fis);
  String[] strs = new String[dis.readInt()];
  for (int i = 0; i < strs.length; i++) {
    strs[i] = dis.readUTF();
    System.out.println(strs[i]);
  }
}
catch (FileNotFoundException e) {
  e.printStackTrace();
}
catch (IOException e) {
  e.printStackTrace();
}
finally {
  if (dis != null) try { dis.close(); } catch (IOException e) {}
  if (fis != null) try { fis.close(); } catch (IOException e) {}
}

One Reply to “09.05 Beliebige Daten lesen und schreiben”

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.