Weitere aktuelle Java-Titel finden Sie bei dpunkt.
 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index


10.4.2

Weitere Streams


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 [1.1] 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.


 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index

Copyright © 2002 dpunkt.Verlag, Heidelberg. Alle Rechte vorbehalten.