Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage
<< | < | > | >> | API | Kapitel 19 - Byte-Streams

## 19.2 Ausgabe-Streams

- 19.2 Ausgabe-Streams
  - 19.2.1 Die Basisklasse OutputStream
  - 19.2.2 Aus OutputStream direkt abgeleitete Klassen
    - FileOutputStream
    - ByteArrayOutputStream
    - ObjectOutputStream
    - PipedOutputStream
  - 19.2.3 Aus FilterOutputStream abgeleitete Klassen
    - BufferedOutputStream
    - PrintStream
    - DataOutput und DataOutputStream
    - Komprimieren von Dateien
    - Berechnung von Prüfsummen

### 19.2.1 Die Basisklasse OutputStream

Basis der Ausgabe-Streams ist die abstrakte Klasse `OutputStream`. Sie stellt folgende Methoden zur Verfügung:

    protected OutputStream()

    public void close()
    public void flush()

    public void write(int b)
    public void write(byte[] b)
    public void write(byte[] b, int offs, int len)
| java.io.OutputStream

Der parameterlose Konstruktor initialisiert einen `OutputStream`. Er ist `protected` und wird in abgeleiteten Klassen überlagert. Mit `close` wird der `OutputStream` geschlossen, und `flush` schreibt die gepufferten Daten physikalisch auf das Ausgabegerät und leert alle Puffer.

Die `write`-Methoden erwarten Bytes oder Byte-Arrays als Daten. Wird ein `byte`-Array angegeben, so gibt die Klasse es vollständig aus, wenn nicht zusätzlich ein Arrayoffset und die Anzahl der zu schreibenden Bytes angegeben wird. Die Methode zum Schreiben eines einzelnen Bytes erwartet ein `int` als Argument, gibt aber lediglich seine unteren 8 Bit aus und ignoriert alle übrigen.

### 19.2.2 Aus OutputStream direkt abgeleitete Klassen

Aus `OutputStream` sind einige weitere Klassen direkt abgeleitet. Wie bei den Character-Streams bestimmen sie im wesentlichen die Art bzw. das Ziel der Datenausgabe.

#### FileOutputStream

Der `FileOutputStream` stellt einen Byte-Stream zur Ausgabe in eine Datei zur Verfügung. Er besitzt folgende Konstruktoren:

    public FileOutputStream(String name)
      throws FileNotFoundException

    public FileOutputStream(String name, boolean append)
      throws FileNotFoundException

    public FileOutputStream(File file)
      throws IOException
| java.io.FileOutputStream

Wird lediglich ein Dateiname angegeben, legt ein `FileOutputStream` die gewünschte Ausgabedatei neu an und setzt den Dateizeiger auf den Anfang der Datei. Wird der zweite Konstruktor verwendet und `true` als zweites Argument übergeben, wird der Dateizeiger auf das Ende der Datei positioniert, falls diese bereits existiert. Andernfalls entspricht das Verhalten dem des ersten Konstruktors. Der dritte Konstruktor entspricht dem ersten, erwartet aber ein `File`-Objekt anstelle eines Strings.

Das folgende Programm zeigt beispielhaft die Anwendung eines `FileOutputStream`. Es legt die in der Kommandozeile angegebene Datei an (bzw. springt zu ihrem Ende, falls sie bereits vorhanden ist) und hängt 256 Bytes mit den Werten 0 bis 255 an.

    001 /* Listing1901.java */
    002
    003 import java.io.*;
    004
    005 public class Listing1901
    006 {
    007   public static void main(String[] args)
    008   {
    009     try {
    010       FileOutputStream out = new FileOutputStream(
    011         args[0],
    012         true
    013       );
    014       for (int i = 0; i < 256; ++i) {
    015         out.write(i);
    016       }
    017       out.close();
    018     } catch (Exception e) {
    019       System.err.println(e.toString());
    020       System.exit(1);
    021     }
    022   }
    023 }
| Listing1901.java

Listing 19.1: Verwendung eines FileOutputStream

#### ByteArrayOutputStream

Die Klasse `ByteArrayOutputStream` schreibt die auszugebenden Daten in ein `byte`-Array, dessen Größe mit dem Datenvolumen wächst. Sie besitzt zwei Konstruktoren:

    public ByteArrayOutputStream()
    public ByteArrayOutputStream(int size)
| java.io.ByteArrayOutputStream

Der parameterlose Konstruktor legt ein Ausgabearray mit einer anfänglichen Größe von 32 Byte an, der andere erlaubt die freie Vorgabe der initialen Puffergröße.

#### ObjectOutputStream

Ein `ObjectOutputStream` erlaubt es, primitive Datentypen und komplette Objekte (inklusive aller referenzierten Objekte) auszugeben. Zwar ist er nicht von `FilterOutputStream` abgeleitet, wird aber ebenso verwendet und erwartet im Konstruktor einen `OutputStream` zur Weiterleitung der Ausgabedaten. Die Klasse `ObjectOutputStream` ist eine der Säulen des Serialisierungs-APIs in Java. Sie wird in Abschnitt 41.1.2 ausführlich beschrieben.

#### PipedOutputStream

Ein `PipedOutputStream` dient zusammen mit einem `PipedInputStream` zur Kommunikation zweier Threads. Beide zusammen implementieren eine Message Queue, in die einer der beiden Threads seine Daten hineinschreibt, während der andere sie daraus liest. Ein ausführliches Beispiel zur Anwendung der beiden Klassen findet sich in Abschnitt 22.4.4.

### 19.2.3 Aus FilterOutputStream abgeleitete Klassen

Die aus `OutputStream` abgeleitete Klasse `FilterOutputStream` ist die Basisklasse aller gefilterten Ausgabe-Streams. Diese definieren kein eigenes Ausgabegerät, sondern bekommen es beim Instanzieren in Form eines `OutputStream`-Arguments übergeben:

    public FilterOutputStream(OutputStream out)
| java.io.FilterOutputStream

Die Aufgabe der aus `FilterOutputStream` abgeleiteten Klassen besteht darin, die Schreibzugriffe abzufangen, in einer für sie charakteristischen Weise zu verarbeiten und dann an das eigentliche Ausgabegerät (den im Konstruktor übergebenen `OutputStream`) weiterzuleiten.

#### BufferedOutputStream

`BufferedOutputStream` puffert die Ausgabe in einen `OutputStream`. Er kann insbesondere dann die Ausgabe beschleunigen, wenn viele einzelne `write`-Aufrufe erfolgen, die jeweils nur wenig Daten übergeben. Ein `BufferedOutputStream` besitzt zwei zusätzliche Konstruktoren und eine Methode `flush`, die dafür sorgt, daß die gepufferten Daten tatsächlich geschrieben werden:

    public BufferedOutputStream(OutputStream out)
    public BufferedOutputStream(OutputStream out, int size)

    public void flush()
      throws IOException
| java.io.BufferedOutputStream

#### PrintStream

Ein `PrintStream` bietet die Möglichkeit, Strings und primitive Typen im Textformat auszugeben. Er stellt eine Vielzahl von `print`- und `println`-Methoden für unterschiedliche Datentypen zur Verfügung. Seine Schnittstelle und Anwendung entspricht der Klasse `PrintWriter`, die in Abschnitt 18.2.3 beschrieben wurde.

|  |  |  | Im Zuge der Internationalisierung des JDK wurden mit der Version 1.1 die öffentlichen Konstruktoren der Klasse `PrintStream` als `deprecated` markiert (wegen der möglicherweise unzulänglichen Konvertierung zwischen Bytes und Zeichen). Damit war die Klasse praktisch nicht mehr verwendbar. Insbesondere war es nicht mehr möglich, die Methoden `setOut` und `setErr` der Klasse `System` sinnvoll zu verwenden (siehe Abschnitt 16.3.2). Später wurde die Entscheidung als falsch angesehen und mit dem JDK 1.2 revidiert. Seither sind die Konstruktoren wieder zulässig. Einen einfach anzuwendenden Workaround, der die `deprecated`-Warnungen im JDK 1.1 vermeidet, gibt es leider nicht. |  |
JDK1.1-1.4

#### DataOutput und DataOutputStream

Ein `DataOutputStream` ermöglicht es, primitive Datentypen in definierter (und portabler) Weise auszugeben. So geschriebene Daten können mit Hilfe eines `DataInputStream` wieder eingelesen werden. Ein `DataOutputStream` implementiert das Interface `DataOutput`, das folgende Methoden enthält:

    void write(int b)
      throws IOException
    void write(byte[] b)
      throws IOException
    void write(byte[] b, int off, int len)
      throws IOException
    void writeBoolean(boolean v)
      throws IOException
    void writeByte(int v)
      throws IOException
    void writeShort(int v)
      throws IOException
    void writeChar(int v)
      throws IOException
    void writeInt(int v)
      throws IOException
    void writeLong(long v)
      throws IOException
    void writeFloat(float v)
      throws IOException
    void writeDouble(double v)
      throws IOException
    void writeBytes(String s)
      throws IOException
    void writeChars(String s)
      throws IOException
    void writeUTF(String str)
      throws IOException
| java.io.DataOutput

Zu jeder einzelnen Methode ist in der JDK-Dokumentation genau angegeben, auf welche Weise der jeweilige Datentyp ausgegeben wird. Dadurch ist garantiert, daß eine mit `DataOutputStream` geschriebene Datei auf jedem anderen Java-System mit einem `DataInputStream` lesbar ist. Die Beschreibungen sind sogar so genau, daß Interoperabilität mit Nicht-Java-Systemen erreicht werden kann, wenn diese in der Lage sind, die primitiven Typen in der beschriebenen Weise zu verarbeiten.

Eine Sonderstellung nimmt die Methode `writeUTF` ein. Sie dient dazu, die 2 Byte langen UNICODE-Zeichen, mit denen Java intern arbeitet, in definierter Weise in 1, 2 oder 3 Byte lange Einzelzeichen zu verwandeln. Hat das Zeichen einen Wert zwischen \u0000 und \u007F, wird es als Einzelbyte ausgeben. Hat es einen Wert zwischen \u0080 und \u07FF, belegt es zwei Byte, und in allen anderen Fällen werden drei Byte verwendet. Diese Darstellung wird als UTF-8-Codierung bezeichnet und ist entsprechend Tabelle 19.1 implementiert. Zusätzlich werden an den Anfang jedes UTF-8-Strings zwei Längenbytes geschrieben.

Von | Bis | Byte | Darstellung
\u0000 | \u007F | 1 | 0nnnnnnn
\u0080 | \u07FF | 2 | 110nnnnn 10nnnnnn
\u0800 | \uFFFF | 3 | 1110nnnn 10nnnnnn 10nnnnnn

Tabelle 19.1: Die UTF-8-Kodierung

Die UTF-8-Kodierung arbeitet bei den gebräuchlichsten Sprachen platzsparend. Alle ASCII-Zeichen werden mit nur einem Byte codiert, und viele andere wichtige Zeichen (insbesondere die im ISO-8859-Zeichensatz definierten nationalen Sonderzeichen, aber auch griechische, hebräische und kyrillische Zeichen), benötigen nur zwei Byte zur Darstellung. Jedes Byte mit gesetztem High-Bit ist Teil einer Multibyte-Sequenz und am ersten Byte einer Sequenz kann die Anzahl der Folgezeichen abgelesen werden.

Wir wollen uns ein Beispielprogramm ansehen:

    001 /* Listing1902.java */
    002
    003 import java.io.*;
    004
    005 public class Listing1902
    006 {
    007   public static void main(String[] args)
    008   {
    009     try {
    010       DataOutputStream out = new DataOutputStream(
    011                              new BufferedOutputStream(
    012                              new FileOutputStream("test.txt")));
    013       out.writeInt(1);
    014       out.writeInt(-1);
    015       out.writeDouble(Math.PI);
    016       out.writeUTF("häßliches");
    017       out.writeUTF("Entlein");
    018       out.close();
    019     } catch (IOException e) {
    020       System.err.println(e.toString());
    021     }
    022   }
    023 }
| Listing1902.java

Listing 19.2: Verwendung der Klasse DataOutputStream

Das Programm erzeugt eine Ausgabedatei test.txt von 38 Byte Länge (wie sie wieder eingelesen wird, zeigt Listing 19.5):

    00 00 00 01 FF FF FF FF-40 09 21 FB 54 44 2D 18   ........@.!.TD-.
    00 0B 68 C3 A4 C3 9F 6C-69 63 68 65 73 00 07 45   ..h....liches..E
    6E 74 6C 65 69 6E                                 ntlein

- Die ersten 4 Byte stellen den int-Wert 1 dar.
- Die nächsten 4 Byte repräsentieren die -1 (es wird die übliche Zweierkomplementdarstellung verwendet).
- Anschließend folgen 8 Byte mit der double-Darstellung der Zahl pi.
- Nun folgt die Längeninformation 000B des ersten UTF-8-Strings und danach dessen 11 Zeichen. Man kann sehen, daß die beiden Umlaute ä und ß jeweils zwei Byte belegen, alle übrigen Zeichen jedoch nur eines.
- Schließlich folgt der zweite UTF-8-String. Er hat die Länge 7, und alle Zeichen werden mit einem Byte dargestellt.

|  |  |  | `DataOutput` wird nicht nur von `DataOutputStream` implementiert, sondern auch von der Klasse `RandomAccessFile`, die darüber hinaus das Interface `DataInput` implementiert. Sollen primitive Daten wahlweise seriell oder wahlfrei verarbeitet werden, ist es daher sinnvoll, die serielle Verarbeitung mit Hilfe der Klassen `DataOutputStream` und `DataInputStream` vorzunehmen. Die Verarbeitung von Random-Access-Dateien wird in Kapitel 20 behandelt. |  |
Tip

#### Komprimieren von Dateien

Die aus `FilterOutputStream` abgeleitete Klasse `DeflaterOutputStream` ist die Basisklasse der beiden Klassen `ZipOutputStream` und `GZIPOutputStream` aus dem Paket `java.util.zip`. Zudem ist `ZipOutputStream` Basisklasse von `JarOutputStream` aus dem Paket `java.util.jar`. Sie alle dienen dazu, die auszugebenden Daten in eine Archivdatei zu schreiben und platzsparend zu komprimieren:

- Das ZIP-Format stammt aus der PC-Welt, ist aber mittlerweile unter praktisch allen Betriebssystemen verfügbar. Es kann mehrere Dateien komprimieren und in einem Archiv zusammenfassen.
- Das GZIP-Format stammt aus der GNU-Welt und kann lediglich eine einzige Datei komprimieren. Zur Erstellung von Archiven, die mehrere Dateien enthalten, wird es meist zusammen mit `tar` verwendet.
- Das JAR-Format entspricht dem ZIP-Format, beinhaltet aber zusätzlich noch Manifest-Dateien, die Metainformationen über die gespeicherten Dateien enthalten.

Das folgende Listing zeigt, wie mehrere Dateien mit Hilfe eines `ZipOutputStream` komprimiert und in eine gemeinsame Archivdatei geschrieben werden. Es wird als Kommandozeilenprogramm aufgerufen und erwartet die Namen der zu erstellenden Archivdatei und der Eingabedateien als Argumente. Das erzeugte Archiv kann mit `jar`, `winzip` oder `pkunzip` ausgepackt werden.

    001 /* Zip.java */
    002
    003 import java.io.*;
    004 import java.util.zip.*;
    005
    006 public class Zip
    007 {
    008   public static void main(String[] args)
    009   {
    010     if (args.length < 2) {
    011       System.out.println("Usage: java Zip zipfile files...");
    012       System.exit(1);
    013     }
    014     try {
    015       byte[] buf = new byte[4096];
    016       ZipOutputStream out = new ZipOutputStream(
    017                             new FileOutputStream(args[0]));
    018       for (int i = 1; i < args.length; ++i) {
    019         String fname = args[i];
    020         System.out.println("adding " + fname);
    021         FileInputStream in = new FileInputStream(fname);
    022         out.putNextEntry(new ZipEntry(fname));
    023         int len;
    024         while ((len = in.read(buf)) > 0) {
    025           out.write(buf, 0, len);
    026         }
    027         in.close();
    028       }
    029       out.close();
    030     } catch (IOException e) {
    031       System.err.println(e.toString());
    032     }
    033   }
    034 }
| Zip.java

Listing 19.3: Erstellen eines ZIP-Archivs

|  | Bitte beachten Sie, daß das Programm nur die Grundzüge des Erstellens von ZIP-Dateien demonstriert. Insbesondere verzichtet es darauf, die verschiedenen Eigenschaften des jeweiligen ZIP-Eintrags korrekt zu setzen (Größe, Datum/Uhrzeit, Prüfsumme etc.). Um dies zu tun, müßten die entsprechenden Daten ermittelt und dann an die jeweiligen `set`-Methoden des `ZipEntry`-Objekts übergeben werden. |  |
Hinweis

#### Berechnung von Prüfsummen

Soll sichergestellt werden, daß Daten während einer Übertragung unverändert bleiben (etwa weil der Übertragungskanal unsicher oder störanfällig ist), werden diese meist mit einer Prüfsumme übertragen. Dazu wird aus den Originaldaten eine Art mathematische Zusammenfassung gebildet und zusammen mit den Daten übertragen. Der Empfänger berechnet aus den empfangenen Daten mit demselben Verfahren die Prüfsumme und vergleicht sie mit der übertragenen. Stimmen beide überein, kann davon ausgegangen werden, daß die Daten nicht verfälscht wurden.

Das Verfahren zur Berechnung der Prüfsumme muß natürlich so beschaffen sein, daß möglichst viele Übertragungsfehler aufgedeckt werden. Oder mit anderen Worten: es soll möglichst unwahrscheinlich sein, daß zwei unterschiedliche Texte (von denen der eine durch Modifikation des anderen enstanden ist) dieselbe Prüfsumme ergeben.

Im JDK können Prüfsummen beim Schreiben von Daten mit der Klasse `CheckedOutputStream` berechnet werden. Sie ist aus `FilterOutputStream` abgeleitet und erweitert deren Schnittstelle um einen Konstruktor zur Auswahl des Prüfsummenverfahrens und um die Methode `getChecksum` zur Berechnung der Prüfsumme. Ihre Anwendung ist einfach und entspricht der Klasse `CheckedInputStream`. Ein Beispiel ist in Listing 19.6 zu finden.

|  | Ein Message Digest ist eine erweiterte Form einer Prüfsumme. Er besitzt zusätzliche Eigenschaften, die ihn für kryptographische Anwendungen qualifizieren, und wird in Abschnitt 47.1.3 erläutert. Die Klasse `DigestOutputStream` dient dazu, einen Message Digest für Daten zu berechnen, die mit einem `OutputStream` ausgegeben werden. Analog dazu gibt es eine Klasse `DigestInputStream` zur Berechnung eines Message Digests für einen `InputStream`. |  |
Hinweis

Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage, Addison Wesley, Version 3.0
<< | < | > | >> | API | © 1998-2002 Guido Krüger, http://www.javabuch.de