Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung
 <<    <     >    >>  Kapitel 19 - Byte-Streams

19.2 Ausgabe-Streams



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.3 

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             

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 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   API  Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0
 <<    <     >    >>  © 2000 Guido Krüger, http://www.gkrueger.com