Piped-Streams
PipedReader und PipedWriter sind zwei Klassen, mit denen
eine Pipe realisiert werden kann. Mit einer Pipe können Streams direkt miteinander gekoppelt
werden, indem einer der Streams mit dem jeweils anderen
verbunden wird.
Die Kommunikation über Piped-Streams ist geeignet für den Datenaustausch zwischen
zwei Threads (siehe Kapitel 11).
Die Kopplung der Streams kann direkt bei der Erzeugung eines Objekts erfolgen.
Hierzu wird dem Konstruktor
des PipedReaders ein Exemplar der Klasse PipedWriter übergeben:
PipedReader p = new PipedReader(myPipedWriter);
Alternativ kann man bei der Erzeugung eines PipedWriters dem
Konstruktor auch ein Exemplar von PipedReader übergeben:
PipedWriter p = new PipedWriter(myPipedReader);
Die Streams können aber auch ohne Übergabe eines
Parameters initialisiert werden:
PipedReader p = new PipedReader();
Die Verbindung dieser Streams kann auch nach der Erzeugung durch einen Aufruf von connect() erfolgen. Die Methode connect() ist sowohl in PipedReader als
auch in PipedWriter enthalten
und erwartet ein Exemplar jeweils anderen Streams als Parameter.
Diese Streams können z. B. eingesetzt werden,
wenn eine Pipeline-Verarbeitung, wie
sie bei UNIX-Befehlen möglich ist, implementiert werden soll.
Das wird nun im folgenden anhand eines Beispiels verdeutlicht.
Es wird zunächst eine abstrakte Klasse Operator definiert.
Unterklassen der Klasse Operator erhalten über einen
Stream Daten, verarbeiten diese Daten und schreiben anschließend
die Ausgabe in einen anderen Stream.
Verschiedene Unterklassen von Operator können
hintereinandergeschaltet werden und sind in der Lage, nebenläufig zu arbeiten.
Deshalb ist die abstrakte Klasse Operator von
Thread abgeleitet:
abstract class Operator extends Thread {
protected Reader in;
protected Writer out;
public Operator(PipedWriter data) throws IOException {
in = new PipedReader(data);
out = new PipedWriter();
}
public Operator(PipedWriter data, Writer out) throws IOException {
in = new PipedReader(data);
this.out = out;
}
public Operator(Reader in) {
this.in = in;
out = new PipedWriter();
}
public Operator(Reader in, Writer out) {
this.in = in;
this.out = out;
}
public Operator() {}
public PipedWriter getPipe() {
if (out instanceof PipedWriter)
return (PipedWriter)out;
else
return null;
}
}
In der Klasse Operator werden
intern zwei Streams in und out definiert,
über die die Ein- und Ausgabe abgewickelt wird.
Zur Initialisierung dieser Datenelemente definiert die Klasse
mehrere Konstruktoren:
public Operator(PipedWriter data) | Dieser Konstruktor wird verwendet, wenn die Eingabe für den Operator
aus einer Pipe kommt und die Ausgabe wieder in eine Pipe geschrieben wird. |
public Operator(PipedWriter data, Writer out) | Wenn die Eingabe aus einer Pipe kommt und die Ausgabe in einen beliebigen
Writer geschrieben wird, verwendet man diesen Konstruktor. |
public Operator(Reader in) | Liest der Operator die Eingabe aus einem beliebigen Reader und
schreibt die Ausgabe in eine Pipe, benutzt man diesen Konstruktor. |
public Operator(Reader in, Writer out) | Dieser Konstruktor wird benutzt, wenn der Operator die Eingabe
aus einem beliebigen Reader liest und die Ausgabe in einen
beliebigen Writer schreibt. |
Beim interessantesten Fall wird sowohl eine Pipe für die
Eingabe als auch für die Ausgabe verwendet:
public Operator(PipedWriter data) throws IOException {
in = new PipedReader(data);
out = new PipedWriter();
}
Der Konstruktor bekommt ein Exemplar der Klasse PipedWriter
übergeben. In diesen Stream werden von einem anderen Thread Daten
geschrieben. Da man die Klasse PipedWriter nicht zum Lesen
von Daten verwenden kann, muss man zunächst einmal eine Umwandlung
in einen PipedReader vornehmen. Das geschieht in diesem
Fall über den Konstruktor:
in = new PipedReader(data);
Da die Ausgabe ebenfalls über eine Pipe erfolgen soll, wird
anschließend ein Exemplar von PipedWriter erzeugt.
Die Ausgabe-Pipe kann an dieser Stelle allerdings noch nicht verbunden
werden, da innerhalb des Operators nicht bekannt ist, welcher
Thread die Ausgabedaten erhält.
Damit man außerhalb der Klasse Operator auf die Ausgabe-Pipe
zugreifen kann, wird die Methode getPipe() definiert:
public PipedWriter getPipe() {
if (out instanceof PipedWriter)
return (PipedWriter)out;
else
return null;
}
getPipe() liefert das PipedWriter-Objekt
zurück, falls die Ausgabe in eine Pipe geschrieben wird.
Dadurch,
dass man in der Klasse Operator mehrere
Konstruktoren definiert, die z. T. auch beliebige Reader- und
Writer-Klassen als Argumente besitzen, hat man die Möglichkeit,
die Ein- und Ausgabe eines Operators direkt auf die Standard-Streams,
Dateien oder Sockets umzuleiten.
Im nächsten Schritt wird ein konkreter Operator erzeugt. Der
Operator hat die Aufgabe, alle Zeilen und Zeichen in der Eingabe
zu zählen. Ist die Eingabe beendet, so wird jeweils die Anzahl an Zeilen und
Zeichen ausgegeben:
class CountOperator extends Operator {
protected LineNumberReader in;
protected PrintWriter out;
public CountOperator(PipedWriter data, Writer out)
throws IOException {
in = new LineNumberReader(new PipedReader(data));
this.out = new PrintWriter(out, true);
}
public CountOperator(Reader in) {
this.in = new LineNumberReader(in);
out = new PrintWriter(new PipedWriter(), true);
}
public void run() {
int count = 0;
int c;
try {
while((c = in.read()) != -1) {
count++;
}
out.println("Lines: "+in.getLineNumber());
out.println("Chars: "+count);
in.close();
out.close();
}
catch(IOException e) {
e.printStackTrace();
}
}
}
Zum Zählen der Zeilen wird ein LineNumberReader verwendet, der
schon im letzten Abschnitt beschrieben wurde. Die Ausgabe erfolgt
über einen PrintWriter. Die eigentliche Funktionalität des
Operators ist in der run()-Methode implementiert.
Zusätzlich
wird eine weitere Unterklasse
von Operator abgeleitet, die
die Aufgabe hat, aus der Eingabe Whitespaces zusammenzufassen.
Mehrere aufeinanderfolgende Leerzeichen oder Tabulatoren werden zu einem
Leerzeichen reduziert. Newline-Zeichen werden hiervon nicht betroffen.
Mit diesen zwei Operatoren ergibt sich das folgende Szenario:
Ein Text wird aus einer Datei ausglesen und anschließend werden die
Whitespaces zusammengefasst. Danach werden von dem CountOperator
die Zeilen und Zeichen gezählt und schließlich ausgegeben.
Die Struktur des DeleteWhitespaceOperators ist
mit der Struktur des CountOperators identisch.
Deshalb wird das Listing an dieser Stelle nicht abgedruckt.
Die Funktionalität des Hauptprogramms besteht lediglich darin, die
einzelnen Operatoren über Pipes miteinander zu verknüpfen und zu starten:
public class PipedStreamsDemo {
public static void main(String argv[]) {
try {
Writer out = new OutputStreamWriter(System.out);
Reader in = new InputStreamReader(System.in);
Operator op1 =
new DeleteWhitespaceOperator(
new FileReader("PipedStreamsDemo.java"));
Operator op2=new CountOperator(op1.getPipe(),out);
op1.start();
op2.start();
}
catch(IOException e) {
e.printStackTrace();
}
}
}
Der Operator DeleteWhitespaceOperator erhält seine Eingabe
von der Datei text.dat. Bei dem hier verwendeten Konstruktor
wird die Ausgabe des Operators in eine Pipe geschrieben.
Der zweite Operator wird mit dem Ausgabe-Stream des ersten Operators
und der Standardausgabe initialisiert. Die Standardausgabe wurde hierzu
zuvor in einen Writer umgewandelt, und die Ausgabe-Pipe des
DeleteWhitespaceOperators über die Methode getPipe()
abgefragt. Da beide Operatoren als Threads implementiert sind, müssen
sie beide durch Aufruf von start() gestartet werden.
In diesem Beispiel werden zwei Operatoren miteinander über Pipes verknüpft,
es ist aber prinzipiell eine Kopplung zwischen beliebig vielen Operatoren
denkbar. Ähnliche Funktionalität,
wie sie hier durch die Operatoren
gezeigt wird, kann man auch durch die Implementierung von Filter-Streams
erreichen. Ein Filter-Stream liest Daten aus einem Stream,
modifiziert die Daten auf irgendeine Weise und schreibt die
Ausgabe anschließend in einen anderen Stream.
Diese Vorgehensweise ist im Unterschied zur obigen Implementierung
von Operatoren immer sequenziell. Die Verarbeitung in den Operatoren dieses Abschnitts
erfolgt hingegen nebenläufig.
Piped-Streams ermöglichen einen asynchronen Austausch großer Datenmengen
zwischen verschiedenen Threads.
In diesem Abschnitt wurden die auf Character-Ebene arbeitenden
Klassen vorgestellt. Analog dazu existieren die Klassen
PipedInputStream und PipedOutputStream, die auf Byte-Basis arbeiten.
Ansonsten gibt es bei ihrer
Verwendung keine Unterschiede zu PipedReader
und PipedWriter.
Array-Streams
Die Klasse CharArrayReader
stellt einen Stream dar, der
Daten aus einem Array liest. Der Konstruktor dieser Klasse bekommt ein Array übergeben, das
die zu lesenden Characters enthält. Die Daten, die sich in diesem Array
befinden, werden wie aus einem normalen Stream ausgelesen.
Der CharArrayWriter ist das Gegenstück zum CharArrayReader.
Daten, die man in diesen
Stream eingibt, werden in ein Array geschrieben.
Dem Konstruktor eines solchen Streams kann die Größe, mit der das Array initialisiert
wird, übergeben werden. Ohne explizite Angabe der Größe
besitzt das Array eine Anfangslänge von 32 Zeichen.
Wird die Länge des Arrays überschritten, dann werden die Daten automatisch intern in ein neues
Array der doppelten Größe umgelagert. Dieser Vorgang kann nicht
vom Programmierer beeinflusst werden.
Mit den Array-Streams kann ein
Zwischenpuffer ohne feste Pufferlänge realisiert werden.
Im Gegensatz dazu ist die Pufferlänge z. B. bei einem Buffered-Stream
fest vorgeschrieben.
Auch der Zielort der Daten wird bei der Verwendung von Array-Streams
flexibel gehalten. Ist das Array beschrieben, könnte man
die Daten z. B. in andere Streams schreiben,
wohingegegen beim BufferedWriter das Ziel der Daten schon feststeht
(nämlich der Stream, mit dem der BufferedWriter verknüpft wurde).
Neben CharArrayReader und CharArrayWriter
enthält die Standardbibliothek die Klassen ByteArrayInputStream
und ByteArrayOutputStream. Diese Klassen verwenden Bytes
statt Characters als Ein- und Ausgabeeinheit, besitzen ansonsten jedoch dieselbe Funktionalität.
SequenceInputStream
Mit der Klasse SequenceInputStream ist es
möglich,
auf einfache Weise sequenziell auf mehrere InputStreams zuzugreifen.
Dem Konstruktor werden entweder zwei einzelne InputStreams
oder ein Exemplar von Enumeration, das Zugriff auf
beliebig viele InputStreams bietet, übergeben.
Die einzelnen Streams werden sequenziell ausgelesen. Signalisiert der aktuelle Stream
mit einem EOF das Ende der Eingabe,
wird auf den jeweils nächsten Stream umgeschaltet.
Da die Datentypen, die über die
einzelnen Streams fließen, unterschiedlich sein
können, sind die Einlesemethoden eines SequenceInputStreams
relativ spartanisch. Komfortablere Methoden können durch
Zwischenschaltung eines FilterInputStreams bereitgestellt werden.
Mit diesen Streams ist es z. B. möglich,
mehrere Dateien sequenziell auszulesen, wie
in folgendem Beispiel gezeigt wird.
An dieser Stelle wird jedoch nur auf die Benutzung der Klasse
SequenceInputStream eingegangen.
Die Dateioperationen werden im nächsten Abschnitt erklärt.
import java.io.*;
import java.util.Vector;
public class SequenceStreamDemo {
public static void main(String args[]) {
// Vector, der die einzelnen Streams aufnimmt
Vector streams = new Vector(3);
PrintWriter stdout = new PrintWriter(System.out, true);
BufferedReader in;
try {
// Erzeugen der Streams und Hinzufügen zum Vector
streams.addElement(new FileInputStream("first.txt"));
streams.addElement(new FileInputStream("second.txt"));
streams.addElement(new FileInputStream("third.txt"));
// Erzeugen eines SequenceInputstreams mit obigem Vector
SequenceInputStream s =
new SequenceInputStream(streams.elements());
in = new BufferedReader(new InputStreamReader(s));
String text;
// Einlesen und anschließendes Ausgeben von Zeichen
// bis zum Erreichen des EOF-Characters
while((text = in.readLine()) != null)
stdout.println(text);
}
catch (IOException e) {
e.printStackTrace();
}
}
}
In diesem Beispiel wird dem Konstruktor ein Exemplar vom Typ Enumeration
übergeben.
Eine Enumeration stellt in Java eine Aufzählung von
Objekten dar.
Sie kann folgendermaßen angelegt werden:
Zuerst wird ein Vector erzeugt,
dem die einzelnen Streams mit der Methode
addElement() hinzugefügt werden. Die elements()-Methode des Vectors
liefert als Ergebnis eine Enumeration mit den einzelnen
Streams, die dem Konstruktor von SequenceInputStream übergeben werden kann.
Danach werden alle Zeichen aus dem SequenceInputStream
ausgelesen und auf der Standardausgabe ausgegeben. Wie man sehen kann, werden alle
drei Streams nacheinander ausgelesen.
Für das sequenzielle Auslesen mehrerer Reader
ist in der Standardbibliothek keine Klasse enthalten.
Näheres über die Klasse Vector und das Interface Enumeration ist
im Kapitel 16 zu finden.
Objekt-Streams
Die
in Version 1.1 eingeführten Objekt-Streams gestatten es, einzelne Objekte, aber auch komplette Graphen von zusammenhängenden Objekten durch Serialisierung persistent zu speichern und wieder zu lesen.
Die Serialisierung ist eine Technik, mit der Objekte in eine Folge von Bytes transformiert werden, die z. B. in Dateien gespeichert oder über ein Netzwerk übertragen werden können. In einem serialisierten Objekt wird die Klasseninformation und der aktuelle Zustand des Objektes zum Zeitpunkt der Serialisierung gespeichert.
Unter Deserialisierung versteht man die Rekonstruktion eines serialisierten Objektes aus seiner serialisierten Form.
Da die Objekt-Serialisierung ein komplexeres Thema ist, soll hier nur eine einfache Möglichkeit zum rekursiven Kopieren erläutert werden.
Das folgende Beispiel dupliziert ein Objekt, das ein Dokument darstellt, durch tiefes Kopieren. Dies bietet sich besonders an, da ein Dokument eine tief verästelte Struktur hat:
Es besteht aus Teilen, die wieder in Kapitel unterteilt sind, und so weiter.
Um die Daten beim Kopieren zu puffern, werden die Objekt-Streams hier über zwei ByteArray-Streams gelegt:
Document javaBook = new Document();
javaBook.addPart(new Part("Die Sprache Java"));
javaBook.addPart(new Part("Programmieren mit Java"));
javaBook.addPart(new Part("Referenz"));
// ObjectOutputStream erzeugen
bufOutStream = new ByteArrayOutputStream();
outStream = new ObjectOutputStream(bufOutStream);
// Objekt im byte-Array speichern
outStream.writeObject(javaBook);
outStream.close();
// Pufferinhalt abrufen
byte[] buffer = bufOutStream.toByteArray();
// ObjectInputStream erzeugen
bufInStream = new ByteArrayInputStream(buffer);
inStream = new ObjectInputStream(bufInStream);
// Objekt wieder auslesen
Document deepCopy;
deepCopy = (Document)inStream.readObject();
Das eigentliche Kopieren erfolgt also mit lediglich zwei Anweisungen:
outStream.writeObject(javaBook);
...
deepCopy = (Document)inStream.readObject();
Der Appletviewer gestattet es, unter dem Menüpunkt »save as« ein Applet in seinem momentanem Zustand zu serialisieren und als Datei abzuspeichern. Ein solches serialisiertes Applet kann dann im Attribut OBJECT des <APPLET>-Tags angegeben werden.
Copyright © 2002 dpunkt.Verlag, Heidelberg. Alle Rechte vorbehalten.