Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung |
<< | < | > | >> | Kapitel 19 - Byte-Streams |
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.
Aus OutputStream sind einige weitere Klassen direkt abgeleitet. Wie bei den Character-Streams bestimmen sie im wesentlichen die Art bzw. das Ziel der Datenausgabe.
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 |
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.
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.
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.
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 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 |
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. |
|
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 |
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. |
|
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 |
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. |
|
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. |
|
Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0 |
<< | < | > | >> | © 2000 Guido Krüger, http://www.gkrueger.com |