![]() |
|
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:
|
|
|
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.
Abb. 5.1: Hierarchie der Klasse Reader
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.
Abb. 5.3: Hierarchie der Klasse InputStream
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. |
|
|
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. |
|
|
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:
Dateiströme Die Dateiströme
import java.io.*; public static void main(String[] args) throws IOException { int zeichen; out.write(c); } } 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:
import java.io.*; public static void main(String[] args) throws IOException { String input; 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.
public static Reader umkehren(Reader quelle) throws IOException { BufferedReader in = new BufferedReader(quelle); } 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.
import java.io.*; private PrintWriter ausgabe = null; this.ausgabe = ausgabe; } if (ausgabe != null && in != null) { try { String input; ausgabe.println(kehreUm(input)); } } catch (IOException e) { } } } if (ausgabe != null) { ausgabe.close(); } in.close(); } } int i, laenge = source.length(); 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:
BufferedReader in = new BufferedReader(quelle); 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). |
|
|
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:
Aneinanderhängen von Streams (Concatenation) Eine Instanz der Klasse
import java.io.*; public static void main(String[] args) throws IOException { int zeichen; 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.
import java.util.*; private String[] dateiListe; this.dateiListe = dateiListe; } if (aktuell < dateiListe.length) return true; else return false; } InputStream in = null; throw new NoSuchElementException("Keine weiteren Daten"); else { String nextElement = listOfFiles[current]; in = new FileInputStream(nextElement); } catch (FileNotFoundException e) { System.err.println("DateiListe: Fehler" + nextElement); } } } } 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:
Ziel dieses Abschnitts ist die Darstellung der Verwendung von Filterströmen. Hierzu ist ein Beispiel angegeben, das ein 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.
DataInputStream dis = new DataInputStream(System.in); // 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. |
|
|
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.
import java.io.*; public static void main(String[] args) throws IOException { // schreiben der Daten out.writeChars(verein[i]); } 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.
DataInputStream in = new DataInputStream(new(FileInputStream("tabelle1.txt")) ; 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.
try { while (true) { punkt = in.readInt(); gesamt += punkt; } } catch (EOFException e) { } } } 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:
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:
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:
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:
import java.io.*; private Pruefsumme pfsumme; super(out); } 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:
Die überschriebenen Methoden sehen wie folgt aus: public void write(int b) throws IOException { out.write(b); } out.write(b, 0, b.length); } out.write(b, off, len); } return pfsumme; } }
import java.io.FilterInputStream; private Checksum pfsumme; super(in); } 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:
public int read() throws IOException { int b = in.read(); pfsumme.update(b); } } int laenge; pfsumme.update(b, 0, len); } } laenge = in.read(b, off, laenge); if (laenge != -1) { pfsumme.update(b, off, laenge); } } 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.
public interface Pruefsumme { /** } private int wert = 1; int s1 = wert & 0xffff; } int s1 = wert & 0xffff; int k = laenge < NMAX ? laenge : NMAX; s1 += b[off++] & 0xff; } } } wert = 1; } return (long)wert & 0xffffffff; } } Die letzte Klasse des Beispiels, IOTest, beinhaltet die main-Methode des gesamten Programms:
import java.io.*; public static void main(String[] args) throws IOException { A32 inChecker = new A32(); in = new CheckedInputStream(new FileInputStream("beispiel1.txt"), inChecker); } catch (FileNotFoundException e) { System.err.println("IOTest: " + e); } catch (IOException e) { System.err.println("IOTest: " + e); } out.write(c); System.out.println("Inputstream-Pruefsumme: " + inChecker.getValue()); } } 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:
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. 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.
FileInputStream einlesen= new FileInputStream("artikel"); 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.
package java.io; // leer } Die Instantiierung serialisierbarer Klassen ist dann sehr einfach, da die Signatur lediglich um die Schlüsselwörter implements Serializable erweitert werden muss.
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 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 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.
private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); } 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.
private void readObject(ObjectInputStream s)throws IOException { s.defaultReadObject(); } 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:
package java.io; public void writeExternal(ObjectOutput out) throws IOException; } Eine externalisierbare Klasse weist stets die folgenden Eigenschaften auf:
Die Methoden 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:
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.
//Lesen der Datei //Schreiben der Datei //Lesen und Schreiben der Datei 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.
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 |
|
|