Streaming
 

Oftmals müssen Anwendungsprogramme Daten von einer externen Quelle lesen bzw. Daten an eine externe Quelle senden. Eine Quelle kann hierbei in Form einer Datei, einer Festplatte, aber auch im Netz oder Hauptspeicher vorliegen. Daten, die gelesen oder übertragen werden sollen, können in Form von Text, Bild, aber auch als Video oder Audio vorliegen. Das Konzept des Streamings ermöglicht einen derartigen Datenaustausch. Aufgrund des allgemeinen Ansatzes werden Spezifika, wie sie bspw. im Rahmen der Netzwerkprogrammierung (siehe Kapitel 5.3) auftreten, nicht betrachtet. Der allgemeine Informationsfluss geschieht hierbei nach folgendem Schema:

  • Laden von Daten
    Ein Programm öffnet einen sog. Strom (Stream) zu einer Informationsquelle (bspw. Datei, Speicher oder Socket). Anschließend werden Daten sequentiell gelesen.
  • Schreiben von Daten
    Ein Programm öffnet einen Strom (Stream) zu einer Empfangssenke. Anschließend werden Daten sequentiell geschrieben.

Die Algorithmen, die zum Lesen und Schreiben von Daten notwendig sind, ähneln sich sehr (siehe Tab. 5-1).

Lesen von Daten

Schreiben von Daten

Öffne Stream

Öffne Stream

Solange noch Daten vorhanden

Solange noch Daten vorhanden

    Lese Daten

    Schreibe Daten

Schließe Stream

Schließe Stream

Tab. 5.1: Lesen und Schreiben von Streams

Das Package java.io stellt die Funktionalität zur Verfügung, die zum Lesen und Schreiben von Streams notwendig ist. Zum einen können die Klassen dieses Packages dahingehend unterschieden werden, ob Zeichen oder Bytes übertragen werden sollen. Zum anderen kann man die Klassen dadurch voneinander abgrenzen, ob Daten geschrieben oder gelesen werden sollen.

In diesem Abschnitt werden zuerst die verschiedenen Typen von Streams beschrieben. Ein besonderes Augenmerk gilt dem Lesen und Schreiben von Objekten über Streams, wozu in Java das Konzept der Objektserialisierung verwendet wird. Dieses Konzept spielt insbesondere im Rahmen der Remote Method Invocation (RMI) eine große Rolle (siehe Kapitel 11.3). Im Anschluss daran wird der wahlfreie Zugriff auf Ströme erläutert, der das Auffinden von Daten in einem sequentiellen Strom ermöglicht, ohne diesen zuvor vollständig durchsuchen zu müssen.

Package java.io

Das Package java.io stellt eine Reihe von Funktionen zur Verfügung, mit denen Ströme realisiert werden können. Zunächst sollen Streams dahingehend unterschieden werden, ob sie Daten zeichenorientiert oder byteorientiert übertragen.

Zeichenorientierte Datenübertragung mit Streams

Zur zeichenorientierten Datenübertragung werden in Java die abstrakten Superklassen Reader und Writer verwendet. Mittels der Klasse Reader können Daten in Form eines lesenden, mittels Writer Daten in Form eines schreibenden Stroms übertragen werden. Beide Klassen beinhalten das Application Programming Interface (API) und Teile der Implementierungen, die zur Datenübertragung von 16 bit-Zeichen notwendig sind.

kap51 

Abb. 5.1: Hierarchie der Klasse Reader

kap52 

Abb. 5.2: Hierarchie der Klasse Writer

Zur tatsächlichen Verwendung der Funktionalität der Klassen Reader und Writer werden Subklassen benutzt, die spezialisierte Streams implementieren. Hierbei können passive und aktive Streams unterschieden werden. Passive Streams leiten Daten lediglich weiter, während aktive Streams zusätzlich eine Datenverarbeitung vornehmen. Die Klassenhierarchie der Klasse Reader ist in Abb. 5-1 aufgeführt, die Hierarchie der Klasse Writer in Abb. 5-2. Hierbei sind passive Stream-Klassen grau unterlegt, aktive Stream-Klassen sind durch einen weißen Hintergrund gekennzeichnet.

Die Verwendung von Reader- und Writer-Objekten empfiehlt sich immer dann, wenn Zeichen des Unicode-Alphabets gelesen oder geschrieben werden sollen. Die im Folgenden vorgestellten Byte-Streams übertragen im Gegensatz dazu 8 bit-Wörter, die nach ISO-Latin-1 kodiert sind.

Byteorientierte Datenübertragung mit Streams

Die byteorientierte Datenübertragung wird meist zum Lesen oder Schreiben von Binärdaten, also bspw. Bildern oder Audio in Form von 8 bit-Werten eingesetzt. Im Package java.io stehen hierzu die Klassen InputStream zum Lesen und OutputStream zum Schreiben zur Verfügung, die ähnliche Eigenschaften aufweisen wie die bereits erläuterten Klassen Reader und Writer. Auch bei dieser Art von Streams kann zwischen passiven und aktiven Strömen unterschieden werden. Die Hierarchie der Klasse InputStream ist in Abb. 5-3 angegeben, die der Klasse OutputStream in Abb. 5-4.

kap53 

Abb. 5.3: Hierarchie der Klasse InputStream

kap54 

Abb. 5.4: Hierarchie der Klasse OutputStream

An dieser Stelle sei besonders auf die Klassen ObjectInputStream und ObjectOutputStream hingewiesen, die im Bereich der Objektserialisierung eine große Rolle spielen.

Die Klassen Reader und InputStream definieren im Hinblick auf die unterschiedlichen Datentypen (zeichenorientiert bzw. byteorientiert) ähnliche APIs. Ein Beispiel hierfür ist in Tab. 5-2 angegeben, wo die Funktionalität der beiden Klassen gegenübergestellt wird. Eine vergleichbare Ähnlichkeit besteht zwischen den Klassen Writer und OutputStream. Hierbei sei angemerkt, dass jegliche Art von Strom automatisch geöffnet wird, wenn ein Strom erzeugt wird (Konstruktoraufruf). Ströme können entweder explizit mit Hilfe der Methode close oder implizit durch die in Java verwendete Garbage Collection geschlossen werden.

Klasse Reader

Klasse InputStream

int read()

int read()

int read(char cbuf[])

int read(byte cbuf[])

int read(char cbuf[], int offset, int length)

int read(byte cbuf[], int offset, int length)

Tab. 5.2: APIs der Klassen Reader und InputStream

Passives Streaming

Passive Streams können Daten in Form von Strings, Dateien oder Pipes lesen oder schreiben, nicht aber Daten manipulieren (siehe aktive Streams). Unter einer Pipe versteht man in diesem Zusammenhang eine Verbindung zweier Programmteile. Die Pipe übernimmt die Ausgaben eines Programmteils und leitet diese als Eingaben an einen anderen Programmteil weiter. Pipes verbinden also stets Programmteile (Threads). In Tab. 5-3 sind die passiven Streams, die Teil des Packages java.io sind, aufgeführt.

Datenquelle

Zeichenorientierte Methode

Byteorientierte Methode

Speicher

CharArrayReader

CharArrayWriter

StringReader

StringWriter

ByteArrayInputStre am

ByteArrayOutputStr eam

StringBufferInputS tream

 

Pipe

PipedReader

PipedWriter

PipedInputStream

PipedOutputStream

Datei

FileReader

FileWriter

FileInputStream

FileOutputStream

Tab. 5.3: Passive Streams im Package java.io

An dieser Stelle ist zu beachten, dass die Klassenpaare der zeichenorientierten Streams Entsprechungen in den byteorientierten Klassen finden, die auf denselben Datenquellen operieren. Im Einzelnen haben die Klassen die folgende Bedeutung:

  • CharArrayReader und CharArrayWriter bzw. ByteArrayInputStream und ByteArrayOutputStream
    Mittels dieser Streams kann Speicher ausgelesen bzw. in den Hauptspeicher geschrieben werden. Hierzu wird ein Array verwendet, mit dessen Lese- und Schreibmethoden Daten aus dem Array gelesen bzw. Daten in den Array geschrieben werden.
  • StringReader und StringWriter bzw. StringBufferInputStream
    Mittels des
    StringReader-Stroms können Zeichen eines Strings, der sich im Hauptspeicher befindet, ausgelesen werden. Analog werden Daten mittels der Klasse StringWriter in einem String gespeichert, wobei die Zeichen zuerst in einem StringBuffer-Objekt gesammelt werden, das anschließend in einen String umgewandelt werden kann. Die Klasse StringBufferInputStream ähnelt der Klasse StringReader bis auf die Tatsache, dass Daten in Form von Bytes aus einem StringBuffer-Objekt gelesen werden.
  • FileReader und FileWriter bzw. FileInputStream und FileOutputStream
    Diese Klassen werden auch als Dateiströme (File Streams) bezeichnet und lesen Daten aus Dateien bzw. schreiben Daten in Dateien des lokalen Dateisystems. Diese Klassen werden zur Verdeutlichung im Folgenden noch näher erläutert.
  • PipedReader und PipedWriter bzw. PipedInputStream und PipedOutputStream
    Diese Klassen implementieren die Funktionalität einer zeichenorientierten bzw. einer byteorientierten Pipe. Auch diese Klassen werden zur Verdeutlichung im Folgenden noch detailliert erläutert.

Dateiströme

Die Dateiströme FileReader, FileWriter, FileInputStream und FileOutputStream lesen bzw. schreiben Dateien des lokalen Dateisystems. Ein Dateistrom kann erzeugt werden, indem ein Dateiname, ein File-Objekt oder ein FileDescriptor-Objekt verwendet wird. Das folgende Beispiel verwendet die Klassen FileReader und FileWriter, um den Inhalt einer Datei beispiel1.txt in eine zweite Datei namens beispiel2.txt zu kopieren.

code 

import java.io.*;
public class KopierOperation {

    public static void main(String[] args) throws IOException {

    int zeichen;
    File inputDatei = new File("beispiel1.txt");
    File outputDatei = new File("beispiel2.txt");
    FileReader einlesen = new FileReader(inputDatei);
    FileWriter ausgabe = new FileWriter(outputDatei);
    while ((zeichen = einlesen.read()) != -1)

      out.write(c);
      in.close();
      out.close();

    }

}

Offensichtlich ist das Programm außerordentlich einfach zu realisieren. Für Ein- und Ausgabedatei wird jeweils ein Deskriptor erzeugt, mit dem die Datei des Dateisystems anschließend referenziert wird.

Pipe-Ströme

PipedReader- und PipedWriter-Objekte zur zeichenorientierten Übertragung bzw. PipedInputStream- und PipedOutputStream-Objekte zur byteorientierten Übertragung implementieren die Ein- und Ausgabe von Pipes, die Daten von einem Thread an einen anderen senden können. Ein wichtiges Einsatzgebiet für Pipes ist die Stapelverarbeitung, bei der die Ausgabe eines Programmteils von einer folgenden Komponente weiter bearbeitet wird. Durch Pipes kann man daher vermeiden, das Resultat eines Bearbeitungsschritts zu speichern, bevor ein weiterer Schritt erfolgen kann. Das folgende Beispiel verdeutlicht die Verwendung von Pipe-Strömen. Hierbei wird eine Datei beispiel.txt eingelesen, deren Worte in umgekehrter Reihenfolge auf dem Bildschirm ausgegeben werden. Zuerst wird die Klasse definiert:

code 

import java.io.*;
public class PipeBeispiel {

    public static void main(String[] args) throws IOException {

    String input;
    FileReader woerter = new FileReader("beispiel.txt");
    //umkehren und sortieren
    Reader worte = umkehren(woerter);
    // Ausgabe auf dem Bildschirm
    BufferedReader eingabe = new BufferedReader(worte);
    while ((input = eingabe.readLine()) != null)

      System.out.println(input);

    in.close();

}

Zum Einlesen der Daten wird hierbei ein FileReader-Objekt angelegt, dessen Ausgabe an ein Pipe-Objekt weitergeleitet wird. Der Pipe-Stream kehrt die Wortreihenfolge wie im Folgenden angegeben um. Hierzu wird innerhalb des Pipe-Objekts ein neuer Thread UmkehrThread angelegt, der die Umkehrung der Worte vornimmt.

code 

    public static Reader umkehren(Reader quelle) throws IOException {

      BufferedReader in = new BufferedReader(quelle);
      PipedWriter pipeAusgabe = new PipedWriter();
      PipedReader pipeEingabe = new PipedReader(pipeAusgabe);
      PrintWriter ausgabe = new PrintWriter(pipeAusgabe);
      new UmkehrThread(ausgabe, in).start();
      return pipeEingabe;

    }

Entscheidend bei der Verwendung einer Pipe ist, dass ein Reader-Objekt angelegt wird, das von einem Writer-Objekt liest. Dies entspricht genau der Definition einer Pipe.

code 

import java.io.*;
public class UmkehrThread extends Thread {

    private PrintWriter ausgabe = null;
    private BufferedReader in = null;
    public UmkehrThread(PrintWriter ausgabe,BufferedReader in){

      this.ausgabe = ausgabe;
      this.in = in;

    }
    public void run() {

      if (ausgabe != null && in != null) {

        try {

          String input;
          while ((input = in.readLine()) != null) {

            ausgabe.println(kehreUm(input));
            ausgabe.flush();

          }
          ausgabe.close();

        } catch (IOException e) {

        }

      }

    }
    protected void finalize() throws IOException {

      if (ausgabe != null) {

        ausgabe.close();
        ausgabe = null;

      }
      if (in != null) {

        in.close();
        in = null;

      }

    }
    private String kehreUm(String quelle) {

      int i, laenge = source.length();
      StringBuffer ziel = new StringBuffer(laenge);
      for (i = (laenge - 1); i >= 0; i--)

        ziel.append(quelle.charAt(i));

      return ziel.toString();

    }

}

Die Methode umkehren enthält einige Anweisungen, die in dieser Art und Weise häufig verwendet werden:

code 

BufferedReader in = new BufferedReader(quelle);
PipedWriter pipeAusgabe = new PipedWriter();
PipedReader pipeEingabe = new PipedReader(pipeAusgabe);
PrintWriter ausgabe = new PrintWriter(pipeAusgabe);

In der ersten Zeile wird ein BufferedReader-Objekt angelegt, das aus einer Quelle Daten einliest. Diese Quelle liegt in Form eines Reader-Objekts vor. Das Programm liest daher Daten direkt aus dem BufferedReader-Objekt, das wiederum aus dem Reader-Objekt liest. Hierdurch wird sichergestellt, dass die readLine-Methode, die Teil der Klasse BufferedReader ist, verwendet werden kann.

Analog hierzu wird das PipedWriter-Objekt auf ein PrintWriter-Objekt abgebildet, so dass die println-Methode des Objekts PrintWriter verwendet werden kann. Derartige Abbildungen finden sehr häufig Verwendung, da hierdurch die verschiedenen Eigenschaften mehrerer Ströme kombiniert werden können.

Aktives Streaming

Im Gegensatz zu passiven Strömen haben aktive Ströme die Aufgabe, während der Datenübertragung Operationen auszuführen (bspw. Zeichenkodierung oder Pufferung). Das Package java.io enthält eine Reihe von Operationen, die während des Schreibens und Lesens ausgeführt werden. Wie auch bei passiven Strömen existieren jeweils Operationspaare, die zur Verarbeitung zeichenorientierter bzw. byteorientierter Ströme eingesetzt werden können (siehe Tab. 5-4).

Prozess

Zeichenorientierte Methode

Byteorientierte Methode

Pufferung

BufferedReader,

BufferedWriter

BufferedInputStream,

BufferedOutputStream

Filterung

FilterReader,

FilterWriter

FilterInputStream,

FilterOutputStream

Konvertierung
Byte <=> Zeichen

InputStreamReader,

OutputStreamWriter

---

Anhängen

(Concatenation)

---

SequenceInputStream

Objektserialisierung

---

ObjectInputStream,

ObjectOutputStream

Datenkonvertierung

---

DataInputStream,

DataOutputStream

Zählfunktion

LineNumberReader

LineNumberInputStrea m

Vorauslesen

PushbackReader

PushbackInputStream

Ausgeben

PrintWriter

PrintStream

Tab. 5.4: Aktive Streams im Package java.io

Auch bei aktiven Strömen stehen daher zeichenorientierte bzw. byteorientierte Streams zur Verfügung, die im Grunde dieselbe Aufgabe erfüllen, allerdings ausgehend von unterschiedlichen Ausgangsdaten. Die Aufgaben der Streams sind im Folgenden erläutert:

  • BufferedReader und BufferedWriter bzw. BufferedInputStream und BufferedOutputStream
    Diese Klassen puffern Daten während des Lesens oder Schreibens. Durch die Pufferung wird die Anzahl der Zugriffe auf die Quelldaten so weit wie möglich eingeschränkt.
  • FilterReader und FilterWriter bzw. FilterInputStream und FilterOutputStream
    Diese abstrakten Klassen definieren das Interface für Filterströme, die Daten während des Lesens oder Schreibens filtern. Diese Stream-Klasse wird im Folgenden noch detailliert erläutert.
  • InputStreamReader und OutputStreamWriter
    Dieses Strompaar fungiert als Brücke zwischen zeichenorientierten und byteorientierten Streams. Ein
    InputStreamReader-Objekt liest Bytes aus einem InputStream-Objekt und konvertiert diese anschließend zu Zeichen, indem Standardverfahren der Zeichenkodierung verwendet werden, oder indem andere Verfahren angewendet werden, die durch einen bestimmten Namen charakterisiert werden. In ähnlicher Art und Weise konvertiert ein OutputStreamWriter-Objekt Zeichen zu Bytes, indem entweder Standardverfahren oder andere Verfahren (siehe oben) zur Kodierung verwendet werden und die Daten anschließend in ein OutputStream-Objekt geschrieben werden. Das jeweilige Standardverfahren der Zeichenkodierung kann bspw. mit dem Befehl System.getProperty("file.encoding") festgestellt werden. Weitere Details der Zeichenkodierung sind in Kapitel 6 (Internationalisierung) angegeben.
  • SequenceInputStream
    Diese Klasse wird dazu verwendet, einen Strom an einen anderen anzuhängen. Diese Stream-Klasse wird im Folgenden noch detailliert erläutert.
  • ObjectInputStream und ObjectOutputStream
    Diese Klassen werden zur Objektserialisierung eingesetzt. Auch diese Stream-Klassen werden im Folgenden noch detailliert erläutert.
  • DataInputStream und DataOutputStream
    Diese Klassen lesen oder schreiben einfache Java-Datentypen in einem plattformunabhängigen Format. Auch diese Stream-Klasse wird im Folgenden noch detailliert erläutert.
  • LineNumberReader bzw. LineNumberInputStream
    Diese Klassen verfolgen die Zeilennummern während des Lesens.
  • PushbackReader bzw. PushbackInputStream
    Diese Klassen sind lesende Streams, die jeweils einen Puffer eines Zeichens/Bytes verwenden. Diese Streams werden eingesetzt, wenn ein Zeichen/Byte vorab gelesen werden soll, um zu entscheiden, wie weiter verfahren werden soll. Es muss allerdings beachtet werden, dass das vorab gelesene Zeichen in den Strom zurückgelegt wird, so dass es nochmals gelesen und weiterverarbeitet werden kann.
  • PrintWriter bzw. PrintStream
    Diese Klassen enthalten komfortable Ausgabemethoden. Da mit diesen Streams Ausgaben außerordentlich einfach getätigt werden können, findet man oftmals eine Abbildung anderer Ströme auf diese Stromart, um die hier enthaltenen Ausgabemöglichkeiten nutzen zu können.

Aneinanderhängen von Streams (Concatenation)

Eine Instanz der Klasse SequenceInputStream erzeugt einen Eingabestrom aus mehreren Input-Quellen. Im folgenden Beispiel wird ein SequenceInputStream-Objekt dazu verwendet, um zwei Dateien in der Reihenfolge aneinander zu fügen, die in der Kommandozeile angegeben ist. In der folgenden Hauptklasse wird die Funktionalität bereits erkennbar.

code 

import java.io.*;
public class Anhaengen {

    public static void main(String[] args) throws IOException {

      int zeichen;
      DateiListe myListe = new DateiListe(args);
      SequenceInputStream s = new SequenceInputStream(myListe);
      while ((zeichen = s.read()) != -1)

        System.out.write(zeichen);

      s.close();

    }

}

Zunächst wird ein DateiListe-Objekt (myListe) erzeugt, das mit den in der Kommandozeile übergebenen Parametern initialisiert wird. Die Parameter geben hierbei an, welche Dateien aneinander gehängt werden sollen. Mittels der Variablen myListe wird auch ein SequenceInputStream-Objekt initialisiert, das myListe verwendet, um einen neuen InputStream für jeden Dateinamen anzulegen, der vom Benutzer angegeben wird. Im Folgenden ist die Klasse DateiListe beschrieben. Da vorab unbekannt ist, wie viele Dateien aneinander gehängt werden sollen, muss nun das Interface Enumeration verwendet und implementiert werden.

code 

import java.util.*;
import java.io.*;
public class DateiListe implements Enumeration {

    private String[] dateiListe;
    private int aktuell = 0;
    public DateiListe(String[] dateiListe) {

      this.dateiListe = dateiListe;

    }
    public boolean hasMoreElements() {

      if (aktuell < dateiListe.length)

        return true;

      else

        return false;

    }
    public Object nextElement() {

      InputStream in = null;
      if (!hasMoreElements())

        throw new NoSuchElementException("Keine weiteren Daten");

      else {

        String nextElement = listOfFiles[current];
        aktuell++;
        try {

          in = new FileInputStream(nextElement);

        } catch (FileNotFoundException e) {

          System.err.println("DateiListe: Fehler" + nextElement);

        }

      }
      return in;

    }

}

Nachdem in der main-Methode ein SequenceInputStream-Objekt erzeugt wurde, liest dieses Objekt Byte für Byte ein. Wenn das SequenceInputStream-Objekt ein InputStream-Objekt für eine neue Datenquelle benötigt, wird die Methode nextElement des Enumeration-Objekts aufgerufen, um ein neues InputStream-Objekt zu erzeugen. Dies tritt immer dann ein, wenn bspw. das Ende eines Eingabestroms erreicht ist. Das DateiListe-Objekt erzeugt FileInputStream-Objekte jeweils einzeln, also immer dann, wenn ein derartiges Objekt tatsächlich verwendet werden soll. Stehen innerhalb von DateiListe keine weiteren Dateien zum Einlesen zur Verfügung, so gibt die Methode nextElement den Wert null zurück und der Aufruf der read-Methode des SequenceInputStream-Objekts gibt den Wert -1 zurück, um das Ende des Einlesevorgangs anzuzeigen. Die derart definierte Funktionalität gibt alle Daten, die von SequenceInputStream eingelesen werden, über die Standardausgabe aus.

Filterung von Datenströmen

Daten, die von einem Strom gelesen werden oder die in einen Strom geschrieben werden, können gefiltert werden, indem ein Filterstrom an den Strom der Originaldaten angefügt wird. Das Package java.io enthält die folgenden Filterströme, die Subklassen der Klassen FilterInputStream oder FilterOutputStream sind:

  • DataInputStream und DataOutputStream
  • BufferedInputStream und BufferedOutputStream
  • LineNumberInputStream
  • PushbackInputStream
  • PrintStream (Ausgabestrom)

Ziel dieses Abschnitts ist die Darstellung der Verwendung von Filterströmen. Hierzu ist ein Beispiel angegeben, das ein DataInputStream- und ein DataOutputStream-Objekt einsetzt. Zusätzlich wird verdeutlicht, wie eigene Filterströme entwikkelt werden können.

Um einen gefilterten Ein- oder Ausgabestrom verwenden zu können, muss ein Filterstrom mit der Ein- oder Ausgabe eines weiteren Streams verbunden werden. Durch diese Vorgehensweise können bspw. die komfortablen read-Methoden, die durch DataInputStream implementiert werden, verwendet werden. Ein Beispiel, in dem ein DataInputStream-Objekt mit dem Standardeingabestrom verbunden ist, ist im folgenden Beispiel angegeben.

code 

DataInputStream dis = new DataInputStream(System.in);
String eingabe;
while ((eingabe = dis.readLine()) != null) {

    // Datenverarbeitung

}

 

Verwendung von DataInputStream und von DataOutputStream

Im Folgenden soll anhand eines Beispiels betrachtet werden, wie die Klassen DataInputStream und DataOutputStream verwendet werden. Im Beispiel werden Tabellen eingelesen und ausgegeben, die Daten über Fußballvereine speichern. Eine Tabelle enthält Spalten, die durch Tabulatorwerte voneinander getrennt sind. Die Spalten enthalten den Namen eines Vereins, die derzeitige Punktzahl und die Anzahl der bisher gewonnenen Spiele. Ein Beispiel hierfür ist in Tab. 5-5 angegeben.

Name des Vereins

Punktezahl

Gewonnene Spiele

Bayern München

50

16

1. FC Kaiserslautern

49

15

Tab. 5.5: Tabelle des Beispiels

Wie andere Filterströme auch muss ein DataOutputStream-Objekt mit einem anderen OutputStream-Objekt verbunden werden. Für dieses Beispiel wird es mit einem FileOutputStream-Objekt verbunden, das in eine Datei tabelle1.txt schreibt. Anschließend werden die Daten, die in Form von Arrays vorliegen, in die Tabelle eingetragen.

code 

import java.io.*;
public class Tabelle {

    public static void main(String[] args) throws IOException {

    // schreiben der Daten
    DataOutputStream out = new DataOutputStream(new(FileOutputStream("tabelle.txt") );
    int[] punkte = { 50, 49};
    int[] gewonnen = { 16, 15};
    String[] verein= { "Bayern München","Kaiserslautern"};
    for (int i = 0; i < punkte.length; i ++) {

      out.writeChars(verein[i]);
      out.writeChar('\t');
      out.writeInt(punkte[i]);
      out.writeChar('\t');
      out.writeInt(gewonnen[i]);
      out.writeChar('\n');

    }
    out.close();
    DataOutputStream dos = new DataOutputStream(new FileOutputStream("tabelle1.txt"));

Nach dem Schreiben der Daten wird der Ausgabe-Stream mittels close geschlossen. Im Anschluss daran wird ein neues DataInputStream-Objekt erzeugt, das auf der Datei operiert, die im vorangegangenen Schritt erzeugt wurde.

code 

    DataInputStream in = new DataInputStream(new(FileInputStream("tabelle1.txt")) ;
    int punkt;
    int gewinn;
    String name;
    int gesamt = 0;

Auch das DataInputStream-Objekt muss mit einem InputStream-Objekt verbunden werden, in diesem Fall mit der Datei, aus der gelesen werden soll (tabelle1.txt). Im Folgenden werden die Daten wieder eingelesen.

code 

    try {

      while (true) {

        punkt = in.readInt();
        in.readChar();       // tab lesen
        gewinn = in.readInt();
        in.readChar();       // tab lesen
        name = in.readLine();
        System.out.println(name +
        " " + punkt + " " + gewinn);
        gesamt += punkt;

      }

    } catch (EOFException e) {

}
System.out.println("Insgesamt vergebene Punkte" + gesamt);
in.close();

}

}

Nachdem alle Daten gelesen wurden, wird ausgegeben, wie viele Punkte in dieser Saison auf die beiden Fußballvereine entfielen. Hierbei sollte der Schleife, in der die Daten eingelesen werden, besondere Beachtung geschenkt werden. Normalerweise sehen derartige Schleifen folgendermaßen aus:

code 

while ((input = dis.readLine()) != null) {

    //Verarbeitung

}

Die readLine-Methode liefert dann den Wert null zurück, wenn das Ende der Datei erreicht ist. Viele der readXXX-Methoden der Klasse DataInputStream können diese Funktionalität allerdings nicht zur Verfügung stellen, da jeder Wert, der zurückgeliefert werden könnte, ein legitimes Zeichen des Datenstroms sein könnte. Aus genau diesem Grund erzeugen alle readXXX-Methoden der Klasse DataInputStream ein EOFException-Objekt. Wird dieses Objekt erkannt, so terminiert die Schleife.

Entwicklung von Filterströmen

Ziel dieses Abschnitts ist die Darstellung, wie gefilterte Ein- und Ausgabesströme selbst entwickelt werden können. Hierzu sind die folgenden Schritte zu durchlaufen:

  1. Erzeugung einer Subklasse der Klassen FilterInputStream und FilterOutputStream. Ein- und Ausgabeströme werden oftmals paarweise verwendet, so dass die Erstellung beider Subklassen sinnvoll ist.
  2. Überschreiben der jeweiligen read- und write-Methoden.
  3. Überschreiben anderer benötigter Methoden.
  4. Überprüfung der Zusammenarbeit von Ein- und Ausgabeströmen.

Auch in diesem Abschnitt wird ein Beispiel verwendet, um die Implementierung eines Filterstroms zu verdeutlichen. Aufgrund seiner hervorragenden didaktischen Eignung soll ein Beispiel verwendet werden, das ursprünglich von David Connelly aus dem Java-Entwickler-Team konzipiert wurde.

Im Beispiel verwenden sowohl Ein- als auch Ausgabestrom eine Prüfsumme, um die geschriebenen bzw. gelesenen Daten abzusichern. Hierbei wird jeweils festgestellt, ob die Prüfsumme der Daten, die vom Input-Stream gelesen wurden, mit der Prüfsumme übereinstimmt, die zu den vom Output-Stream geschriebenen Daten gehört. Das Beispiel besteht aus vier Klassen und einem Interface:

  • den Subklassen der Filterklassen CheckedOutputStream und CheckedInputStream.
  • dem Interface Pruefsumme und der Klasse A32 zur Berechnung der Checksumme der Ströme.
  • der Klasse IOTest, die die main-Methode des Programms enthält.

Die Klasse CheckedOutputStream ist eine Subklasse der Klasse FilterOutputStream, die eine Prüfsumme der Daten berechnet, die in den Stream hineingeschrieben werden. Wird eine Instanz der Klasse CheckedOutputStream erzeugt, so muss der Konstruktor wie folgt aufgerufen werden:

code 

import java.io.*;
public class CheckedOutputStream extends FilterOutputStream {

    private Pruefsumme pfsumme;
    public CheckedOutputStream(OutputStream out, Pruefsumme pfsumme) {

      super(out);
      this.pfsumme = pfsumme;

    }

Der Konstruktor erwartet ein Argument vom Typ OutputStream sowie eines vom Typ Pruefsumme. Das OutputStream-Argument entspricht dem Ausgabestrom, der die Instanz vom Typ CheckedOutputStream filtern soll. Das Pruefsumme-Argument ist ein Objekt, das eine Prüfsumme berechnen kann. Das CheckedOutputStream-Objekt initialisiert sich hierbei selbst, indem der Konstruktor der Superklasse aufgerufen wird und die Variable pfsumme, die als private gekennzeichnet ist, mit dem Pruefsumme-Objekt initialisiert wird. Das CheckedOutputStream-Objekt verwendet pfsumme, um die Prüfsumme jedes Mal zu aktualisieren, wenn Daten in den Strom hineingeschrieben werden.

CheckedOutputStream muss die write-Methoden der Klasse FilterOutputStream überschreiben, so dass jedes Mal die Prüfsumme berechnet wird, wenn die write-Methode ausgeführt wird. In der Klasse FilterOutputStream stehen die folgenden drei Varianten der write-Methode zur Verfügung:

  • write(int i)
  • write(byte[] b)
  • write(byte[] b, int offset, int laenge)

Die überschriebenen Methoden sehen wie folgt aus:

code 

    public void write(int b) throws IOException {

      out.write(b);
      pfsumme.update(b);

    }
    public void write(byte[] b) throws IOException {

      out.write(b, 0, b.length);
      pfsumme.update(b, 0, b.length);

    }
    public void write(byte[] b, int off, int len) throws IOException {

      out.write(b, off, len);
      pfsumme.update(b, off, len);

    }
    public Checksum getPruefsumme() {

      return pfsumme;

    }

}

Die Klasse CheckedInputStream ähnelt der Klasse CheckedOutputStream. Als Subklasse von FilterInputStream berechnet die Klasse CheckedInputStream eine Prüfsumme der Daten, die aus einem Stream gelesen werden. Zunächst muss der Konstruktor wie folgt aufgerufen werden:

code 

import java.io.FilterInputStream;
import java.io.InputStream;
import java.io.IOException;
public class CheckedInputStream extends FilterInputStream {

    private Checksum pfsumme;
    public CheckedInputStream(InputStream in, Pruefsumme pfsumme) {

      super(in);
      this.pfsumme = pfsumme;

    }

Anschließend muss die Klasse CheckedInputStream die read-Methoden der Klasse FilterInputStream überschreiben, so dass jedes Mal, wenn Daten gelesen werden, eine neue Prüfsumme berechnet wird. Auch in der Klasse FilterInputStream sind drei read-Methoden definiert:

code 

    public int read() throws IOException {

      int b = in.read();
      if (b != -1) {

        pfsumme.update(b);

      }
      return b;

    }
    public int read(byte[] b) throws IOException {

      int laenge;
      laenge = in.read(b, 0, b.length);
      if (laenge != -1) {

        pfsumme.update(b, 0, len);

      }
      return laenge;

    }
    public int read(byte[] b, int off, int laenge) throws IOException {

      laenge = in.read(b, off, laenge);

      if (laenge != -1) {

        pfsumme.update(b, off, laenge);

      }
      return laenge;

    }
    public Checksum getPruefsumme() {

      return pfsumme;

    }

}

Anschließend muss der Interface Pruefsumme implementiert werden, das vier Methoden für Prüfsummenobjekte definiert. Diese Methoden setzen den Wert der Prüfsumme zurück, aktualisieren ihn oder liefern den Wert zurück. Im Beispiel wird im Folgenden die Adler32-Prüfsumme implementiert, die erheblich schneller ist als bspw. die CRC32-Checksumme, wenn auch etwas weniger zuverlässig. Da im Beispiel aber eine fortwährende Berechnung der Prüfsumme gewünscht ist, muss die notwendige Berechnung vor allem schnell erfolgen.

code 

public interface Pruefsumme {

    /**
    * Aktualisieren der Pruefsumme mit angegebenem Byte */
    public void update(int b);
    /**
    * Aktualisieren der Pruefsumme mit angegebenem Array aus
    * Bytes */
    public void update(byte[] b, int off, int len);
    /**
    * Zurueckgeben des Werts der Pruefsumme */
    public long getValue();
    /**
    * Zuruecksetzen der Checksumme */
    public void reset();

}
public class A32 implements Pruefsumme {

    private int wert = 1;
    /* BASIS ist die groesste Primzahl kleiner als 65536
    * NMAX ist das groesste n so dass 255n(n+1)/2 +
    * (n+1)(BASIS-1) <= 2^32-1*/
    private static final int BASIS = 65521;
    private static final int NMAX = 5552;
    public void update(int b) {

      int s1 = wert & 0xffff;
      int s2 = (wert >> 16) & 0xffff;
      s1 += b & 0xff;
      s2 += s1;
      wert = ((s2 % BASIS) << 16) | (s1 % BASIS);

    }
    public void update(byte[] b, int off, int laenge) {

      int s1 = wert & 0xffff;
      int s2 = (wert >> 16) & 0xffff;
      while (laenge > 0) {

        int k = laenge < NMAX ? laenge : NMAX;
        laenge -= k;
        while (k-- > 0) {

          s1 += b[off++] & 0xff;
          s2 += s1;

        }
        s1 %= BASIS;
        s2 %= BASIS;

      }
      wert = (s2 << 16) | s1;

    }
    public void reset() {

      wert = 1;

    }
    public long getValue() {

      return (long)wert & 0xffffffff;

    }

}

Die letzte Klasse des Beispiels, IOTest, beinhaltet die main-Methode des gesamten Programms:

code 

import java.io.*;
public class IOTest {

    public static void main(String[] args) throws IOException {

      A32 inChecker = new A32();
      A32 outChecker = new A32();
      CheckedInputStream in = null;
      CheckedOutputStream out = null;
      try {

        in = new CheckedInputStream(new FileInputStream("beispiel1.txt"), inChecker);
        out = new CheckedOutputStream( new FileOutputStream("beispiel2.txt"), outChecker);

      } catch (FileNotFoundException e) {

        System.err.println("IOTest: " + e);
        System.exit(-1);

      } catch (IOException e) {

        System.err.println("IOTest: " + e);
        System.exit(-1);

      }
      int c;
      while ((c = in.read()) != -1)

        out.write(c);

      System.out.println("Inputstream-Pruefsumme: " + inChecker.getValue());
      System.out.println("Outputstream-Pruefsumme: " + outChecker.getValue());
      in.close();
      out.close();

    }

}

Das Hauptprogramm erzeugt zwei A32-Prüfsummenobjekte. Das erste Objekt wird hierbei zur Überprüfung des Ausgabe-Streams, das zweite Objekt zur Überprüfung des Eingabe-Streams verwendet. Der Einsatz zweier Prüfsummen ist hierbei notwendig, da die Prüfsummenobjekte während der Schreib- und Leseaufrufe aktualisiert werden und derartige Aufrufe parallel erfolgen können.

Objektserialisierung

Die Verarbeitung von Objekten mittels Streams, für die in Java die Klassen ObjectInputStream und ObjectOutputStream zur Verfügung stehen, unterscheidet sich deutlich von der Verwendung anderer Stream-Arten. Um ein Objekt speichern zu können, muss der Zustand des Objekts zum Speicherzeitpunkt in einer seriellen Form geschrieben werden. Die gespeicherten Daten müssen hierbei erlauben, das Objekt wieder einzulesen und damit weiterzuarbeiten, als ob keine Zwischenspeicherung erfolgt wäre. Den Prozess des Schreibens und Lesens von Objekten bezeichnet man in diesem Kontext auch als Objektserialisierung. Einsatzgebiete der Objektserialisierung sind:

  • Remote Method Invocation (RMI, siehe auch Kapitel 11) zur Objektkommunikation über Sockets.
  • Persistenz, also die Archivierung eines Objekts, das zu einem späteren Zeitpunkt der Programmabarbeitung wieder benötigt wird.

In den folgenden Unterkapiteln wird zunächst beschrieben, wie Objekte serialisiert werden müssen. Anschließend wird betrachtet, wie eine Klasse implementiert werden muss, damit ihre Instanzen serialisiert werden können.

Serialisierung von Objekten

Die Serialisierung von Objekten erfolgt immer in zwei Schritten. Zuerst muss ein Objekt in einen Strom geschrieben und anschließend wieder rekonstruiert werden.

Um Objekte in einen Stream schreiben zu können, muss ein Objekt zunächst erzeugt und anschließend serialisiert werden. Im folgenden Beispiel werden eine Zeichenkette und eine Zahl serialisiert.

code 

int i = 20;
FileOutputStream ausgabe= new FileOutputStream("artikel");
ObjectOutputStream s = new ObjectOutputStream(ausgabe);
s.writeObject("Artikel: Seife");
s.writeObject(new Integer(i));
s.flush();

Da die Klasse ObjectOutputStream die Funktion eines verarbeitenden Stroms erfüllt, muss zunächst ein Strom erzeugt werden, auf dem eine ObjectOutputStream-Instanz operieren kann. Hierbei wird ein FileOutputStream-Objekt erzeugt, das das serialisierte Objekt in einer Datei mit dem Namen artikel speichert.

Bezieht sich ein Objekt auf andere Objekte, so müssen alle Objekte, die vom ersten referenziert werden, zur selben Zeit gespeichert werden wie das erste Objekt. Durch dieses Vorgehen werden auch die Referenzen konsistent gespeichert.

Die Klasse ObjectOutputStream implementiert das Interface DataOutput, das eine Reihe von Methoden definiert, mit denen einfache Datentypen gespeichert werden können, bspw. die Methoden writeInt, writeFloat und writeUTF. Die Methode writeObject erzeugt eine Ausnahme vom Typ NotSerializableException, wenn ein Objekt verwendet wird, das nicht serialisierbar ist. Ein Objekt ist genau dann serialisierbar, wenn die Objektklassen das Interface Serializable implementieren.

Nachdem Objekte und einfache Datentypen in einen Strom geschrieben wurden, können sie später wieder gelesen und rekonstruiert werden. Im folgenden Beispiel werden die Objekte, die im ersten Beispiel gespeichert wurden, wieder eingelesen.

code 

FileInputStream einlesen= new FileInputStream("artikel");
ObjectInputStream s = new ObjectInputStream(einlesen);
String today = (String)s.readObject();
Integer zahl = (Int)s.readObject();

In Analogie zur Klasse ObjectOutputStream muss auch ein Objekt vom Typ ObjectInputStream auf einem anderen Stream aufgesetzt werden. Im Beispiel wurden die Daten in einer Datei archiviert, so dass das ObjectInputStream-Objekt auf einem FileInputStream-Objekt aufsetzen muss. Anschließend wird die Methode readObject, die Teil der Klasse ObjectInputStream ist, verwendet, um die Daten wieder einzulesen. Die Reihenfolge, in der Daten gelesen werden, entspricht stets der Reihenfolge, in der die Daten gespeichert wurden. Es ist weiterhin zu beachten, dass das Objekt, das von der Methode readObject als Ergebnis geliefert wird, auf einen spezifischen Typ abgebildet (Casting) werden muss. Um Referenzen zwischen Objekten wieder herzustellen, deserialisiert die Methode readObject das jeweils nächste Objekt eines Stroms und geht rekursiv die Referenzen zu allen Objekten durch, die vom ersten Objekt aus erreichbar sind. Diese Objekte werden anschließend rekonstruiert.

Ströme der Klasse ObjectInputStream implementieren das Interface DataInput, das Methoden zum Lesen einfacher Datentypen zur Verfügung stellt. Diese Methoden entsprechen denen, die das Interface DataOutput zum Schreiben einfacher Datentypen anbietet, bspw. readInt, readFloat und readUTF.

Verwendung der Objektserialisierung

Ein Objekt kann dann serialisiert werden, wenn seine Klassen das Interface Serializable implementieren. Dies ist vor allem deshalb leicht umzusetzen, da das Interface leer ist. Serializable beinhaltet also keine Methodendefinitionen, sondern dient ausschließlich der Identifikation serialisierbarer Klassen.

code 

package java.io;
public interface Serializable {

    // leer

}

Die Instantiierung serialisierbarer Klassen ist dann sehr einfach, da die Signatur lediglich um die Schlüsselwörter implements Serializable erweitert werden muss.

code 

public class SerialisierbareKlasse implements Serializable {

    // Inhalt

}

Zur Implementierung müssen keinerlei Methoden geschrieben werden. Die Serialisierung der Instanzen einer Klasse wird von der Methode defaultWriteObject der Klasse ObjectOutputStream übernommen, die automatisch alle Daten anlegt, die zur Rekonstruktion der Instanz einer Klasse notwendig sind:

  • Die Klasse eines Objekts.
  • Die Signatur einer Klasse.
  • Werte aller nichttemporären und nichtstatischen Variablen, einschließlich der Referenzen zu anderen Objekten.

Die meisten Klassen können auf diese Art und Weise serialisiert werden. Es ist allerdings zu bedenken, dass die Standardserialisierung sehr langsam sein kann. Aus diesem Grund kann es durchaus sinnvoll sein, wenn eine Klasse die Serialisierung explizit kontrolliert.

Serialisierungskontrolle

Die Serialisierung kann kontrolliert werden, wenn die Methoden writeObject und readObject überschrieben werden. Die Methode writeObject kontrolliert, welche Informationen gespeichert werden. Üblicherweise werden hiermit Informationen an einen Strom angehängt. Die Methode readObject kann entweder dazu eingesetzt werden, Informationen wiederherzustellen, die von der entsprechenden writeObject-Methode gespeichert wurden, oder, um den Zustand eines Objekts zu aktualisieren, das bereits wiederhergestellt wurde.

Die Methode writeObject muss exakt in der Art und Weise deklariert werden, die im Folgenden angegeben ist. Hierbei sollte als erster Serialisierungsschritt die Methode defaultWriteObject eines Stroms aufgerufen werden. Spezielle Vereinbarungen folgen dann als weitere Schritte.

code 

private void writeObject(ObjectOutputStream s) throws IOException {

    s.defaultWriteObject();
    // spezieller Serialisierungs-Code

}

Die Methode readObject muss alle Daten in derselben Reihenfolge einlesen, wie diese von der Methode writeObject gespeichert wurden. Die Methode readObject kann weiterhin Berechnungen oder Aktualisierungen des Objektzustandes vornehmen. Die im Folgenden angeführte Methode readObject ist das Gegenstück der oben aufgelisteten Methode writeObject.

code 

private void readObject(ObjectInputStream s)throws IOException  {

    s.defaultReadObject();
    // spezieller Deserialisierungs-Code
    // Objektaktualisierungen

}

Die Methoden writeObject und readObject serialisieren ausschließlich eine speziell ausgezeichnete Klasse. Serialisierungen, die von den Superklassen gefordert werden, werden hierbei automatisch ausgeführt. Möchte eine Klasse die Serialisierung aber explizit mit ihren Superklassen koordinieren, so muss das Interface Externalizable implementiert werden.

Interface Externalizable

Eine vollständige explizite Kontrolle des Serialisierungsprozesses erfordert die Implementierung des Interfaces Externalizable. Objekte, die dieses Interface implementieren, gewährleisten, dass lediglich die Identität der Objektklasse von einem Strom automatisch gespeichert wird. Die Klasse ist für das Schreiben und Lesen ihrer Inhalte selbst verantwortlich und muss diesen Vorgang mit ihren Superklassen abstimmen. Die Definition dieses Interfaces ist im Folgenden angegeben:

code 

package java.io;
public interface Externalizable extends Serializable {

    public void writeExternal(ObjectOutput out) throws IOException;
    public void readExternal(ObjectInput in)throws IOException, java.lang.ClassNotFoundException;

}

Eine externalisierbare Klasse weist stets die folgenden Eigenschaften auf:

  • die Klasse implementiert das Interface java.io.Externalizable.
  • die Klasse implementiert die Methode writeExternal, um den Zustand eines Objekts speichern zu können. Weiterhin muss eine explizite Koordination mit einem Supertyp stattfinden, um den Zustand zu speichern.
  • die Klasse implementiert eine readExternal-Methode, um Daten einlesen zu können, die von der writeExternal-Methode eines Stroms gespeichert wurden, und um den Zustand eines Objekts wiederherstellen zu können. Auch hierbei muss eine explizite Koordination mit dem Supertyp stattfinden, um den Zustand wieder einlesen zu können.
  • wird ein extern definiertes Format gespeichert, so sind ausschließlich die Methoden writeExternal und readExternal für die Verarbeitung dieses Formats zuständig.

Die Methoden writeExternal und readExternal sind als public deklariert. Hierbei besteht die Gefahr, dass ein Anwender Informationen in ein Objekt schreiben bzw. aus diesem lesen kann, die sich von den Methoden und Feldern unterscheiden. Diese beiden Methoden dürfen daher nur dann verwendet werden, wenn die Verfügbarkeit dieser Informationen kein Risiko darstellt.

Schutz von Informationen

Wird eine Klasse entwickelt, die einen kontrollierten Zugriff auf Ressourcen gewährleistet, so muss der Schutz von Informationen und Funktionen umgesetzt werden. Während der Serialisierung wird der als private gekennzeichnete Zustand eines Objekts wiederhergestellt. Wird bspw. ein Datei-Handle verwendet, so finden Zugriffe auf Ressourcen des Betriebssystems statt. Sollte das Datei-Handle verändert werden können, so können unberechtigte Zugriffe nicht ausgeschlossen werden. Eine Serialisierungsumgebung muss daher beachten, dass Ströme auf konservative Art und Weise verarbeitet werden, bspw. durch eine Verifikation von Daten durch eine weitere Klasse.

In Java stehen verschiedene Techniken zur Verfügung, um sensible Daten zu schützen. Die einfachste Möglichkeit besteht darin, Felder, die sensible Daten enthalten, als temporär zu markieren. Temporäre Felder werden nicht serialisiert.

Besonders empfindliche Klassen sollten überhaupt nicht serialisiert werden. Um dies zu erreichen, sollte ein Objekt weder das Interface Serializable noch das Interface Externalizable implementieren.

In einigen Klassen kann es sinnvoll sein, Schreib- und Lesezugriffe zu erlauben, zusätzlich aber den Zustand speziell zu überprüfen und geeignet zu reagieren, wenn deserialisiert wird. Derartige Klassen sollten die Methoden writeObject und readObject implementieren, um nur ausgewählte Zustandsbestandteile zu speichern und wieder einzuladen. Wenn ein Zugriff verhindert werden soll, kann eine Klasse die Exception NotSerializableException generieren, um den weiteren Zugriff zu verhindern.

Wahlfreier Zugriff

Input- und Output-Streams, die bisher beschrieben wurden, greifen stets sequentiell auf Daten zu. Diese Vorgehensweise ist vergleichbar mit dem Lesen magnetischer Bänder, von denen Informationen in der Reihenfolge gelesen werden können, in der sie abgespeichert wurden. Oftmals ist es aber notwendig, Informationen wahlfrei abzurufen, ohne jeweils die gesamten Daten vor der aktuellen Position ebenfalls einlesen zu müssen. Betrachtet man eine Festplatte, so wäre es außerordentlich lästig, alle Dateien lesen zu müssen, die sich physikalisch vor derjenigen Datei befinden, die tatsächlich benötigt wird. Eine Verzeichnisstruktur, die die Organisation einer Festplatte angibt, könnte in diesem Fall auch nicht verwendet werden.

Ein wahlfreier Zugriff auf gespeicherte Informationen läuft in der Regel in folgenden Schritten ab:

  1. Öffnen eines Speichermediums, das den wahlfreien Zugriff unterstützt
  2. Anzeige der Verzeichnisstruktur und Auswahl der gewünschten Daten (bspw. in Form einer Datei)
  3. Lokalisierung der Position der gewünschten Daten im Speichermedium
  4. Lesen und Manipulieren der benötigten Daten
  5. Schließen des Speichermediums

Zur Realisierung wahlfreier Zugriffe auf Dateien steht in Java die Klasse RandomAccessFile, die Teil des Packages java.io ist, zur Verfügung. Diese Klasse wird sowohl zum Lesen als auch zum Schreiben von Dateien verwendet und unterscheidet sich diesbezüglich von anderen Input- und Output-Streams in Java. Zur Angabe, ob Dateien gelesen oder geschrieben werden sollen, muss ein RandomAccessFile-Objekt mit unterschiedlichen Argumenten angelegt werden.

In der Klassenhierarchie des Packages java.io ist die Klasse RandomAccessFile von den anderen Input- und Output-Streams unabhängig, da sie ihre Eigenschaften weder von der Klasse InputStream noch von der Klasse OutputStream erbt. Hieraus ergeben sich einige Nachteile, da die Verwendung von Filtern nicht in derselben Art und Weise erfolgen kann wie bei anderen Strömen. Die Klasse RandomAccessFile implementiert allerdings die Interfaces DataInput und DataOutput, so dass Filter, die entweder für das Interface DataInput oder für das Interface DataOutput entwickelt wurden, sowohl für Dateien mit sequentiellem Zugriff als auch für Dateien mit wahlfreiem Zugriff funktionieren. Hierbei sind die Klassen, die den sequentiellen Zugriff anbieten, diejenigen, die die Interfaces DataInput oder DataOutput implementieren.

Die Klasse RandomAccessFile implementiert die Interfaces DataInput und DataOutput und kann daher sowohl zum Lesen als auch zum Schreiben verwendet werden. In dieser Funktionsweise ähnelt diese Klasse den Klassen FileInputStream und FileOutputStream, da eine Datei des Dateisystems angegeben wird, die in dem Moment geöffnet wird, in dem sie erzeugt wird. Wird eine Instanz der Klasse RandomAccessFile erzeugt, so muss angegeben werden, ob diese eine Datei liest oder schreibt. Dies impliziert, dass die Rechte zum Lesen und Schreiben von Dateien eines Dateisystems entsprechend gesetzt sein müssen. Im folgenden Beispiel wird jeweils eine Instanz der Klasse RandomAccessFile angelegt, die zum Lesen, zum Schreiben bzw. zum Lesen und Schreiben der Datei beispiel.txt verwendet werden kann. Hierbei wird die jeweilige Funktionalität als Buchstabenkombination aus r (read bzw. lesen) und w (write bzw. schreiben) angegeben.

code 

//Lesen der Datei
new RandomAccessFile("beispiel.txt", "r");

//Schreiben der Datei
new RandomAccessFile("beispiel.txt", "w");

//Lesen und Schreiben der Datei
new RandomAccessFile("beispiel.txt", "rw");

Nach dem Öffnen einer Datei können die bereits vorgestellten Methoden readXXX oder writeXXX dazu verwendet werden, um Daten zu lesen oder zu schreiben. Die Klasse RandomAccessFile unterstützt das Konzept eines Dateizeigers, der auf die momentane Position innerhalb einer Datei zeigt. Wird eine Datei angelegt, so ist der Wert dieses Zeigers null (Anfang der Datei). Aufrufe der Methoden readXXX und writeXXX erhöhen den Wert des Dateizeigers um die Anzahl der Bytes, die gelesen bzw. geschrieben wurden. Zusätzlich zu den Methoden, die den Wert des Dateizeigers implizit verändern, enthält die Klasse RandomAccessFile drei Methoden, mit denen der Wert des Zeigers explizit verändert werden kann.

  • skipBytes
    bewegt den Dateizeiger um die angegebene Anzahl von Bytes vorwärts.
  • seek
    positioniert den Dateizeiger vor das spezifizierte Byte.
  • getFilePointer
    gibt die Position des Dateizeigers zurück.

Zusammenfassung

Ziel dieses Unterkapitels ist die Darstellung der Verwendung von Strömen, mit denen allgemein Daten von einer Quelle zu einer Senke übertragen werden können. Die notwendigen Details wurden im Rahmen dieses Unterkapitels erläutert. Im Folgenden wird die Sicherheitsproblematik in Java-Anwendungen betrachtet.


SPNavRight SPNavRight SPNavRight
BuiltByNOF