08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 691
5.4.3 Ströme zur Ein- und Ausgabe
Ein- und Ausgabe von Daten wird heutzutage meist durch Ströme modelliert.
Begriffsklärung: (Datenstrom)
Ein Strom ist eine potentiell unendliche Folge von Daten. Er wird von einer oder mehrerer Quellen mit Daten versorgt und erlaubt es, diese Daten der Reihe nach aus dem Strom herauszulesen.
Das Ende eines Stromes wird durch ein spezielles Datum (in Java ist das -1) markiert.
Sowohl beim Schreiben in einen Strom als auch beim Lesen aus einen Strom kann es zu Verzögerungen kommen:
- beim Lesen, weil augenblicklich kein Zeichen vorhanden, der Strom aber noch nicht zu Ende ist;
- beim Schreiben, weil ggf. kein Platz im Strom vorhanden ist.
Die Verzögerungen führen zu einer Blockierung der ausgeführten Methode.
08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 692
Bemerkung:
• Stromklassen sind in diesem Kontext interessant:
- als wichtige Programmierschnittstelle
- als Beispiel für Typhierarchien und eigenständige Bausteine
- als Beispiel für Komposition von Bausteinen
• Vergleiche auch unendliche Listen in ML (Folie 215).
Einführung in Ströme
Wir betrachten zunächst Ströme zum Lesen:
interface CharEingabeStrom { int read() throws IOException;
}
Diese Schnittstelle abstrahiert von der Quelle aus der gelesen wird. Mögliche Quellen:
1. Datenstruktur wie Feld, Liste, String.
2. Datei 3. Netzwerk
4. Standardeingabe, d.h. interaktive vom Anwender 5. andere Programme
6. andere Ströme
Wir betrachten hier die Fälle 1, 2 und 6.
Lesen aus einer Datenstruktur:
Wir betrachten das schrittweise Lesen der Zeichen eines Strings:
public class StringLeser
implements CharEingabeStrom { private char[] dieZeichen;
private int index = 0;
public StringLeser( String s ) { dieZeichen = s.toCharArray();
}
public int read() {
if( index == dieZeichen.length ) return -1;
else return dieZeichen[index++];
} }
Die Quelle des Stroms wird dem Stromkonstruktor übergeben.
Zusammenbauen von Strömen:
Wir betrachten zunächst zwei Stromklassen, die aus anderen Strömen lesen und diese ändern.
public class GrossBuchstabenFilter
implements CharEingabeStrom { private CharEingabeStrom eingabeStrom;
public GrossBuchstabenFilter(
CharEingabeStrom cs ) { eingabeStrom = cs;
}
public int read() throws IOException { int z = eingabeStrom.read();
if( z == -1 ) { return -1;
} else {
return Character.toUpperCase( (char)z );
} } }
Der Konstruktor nimmt einen beliebigen CharEingabeStrom als Quelle:
Subtyping at its best!
08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 695
public class UmlautSzFilter
implements CharEingabeStrom { private CharEingabeStrom eingabeStrom;
private int puffer = -1;
public UmlautSzFilter( CharEingabeStrom cs ){
eingabeStrom = cs;
}
public int read() throws IOException { if( puffer != -1 ) {
int z = puffer;
puffer = -1;
return z;
} else {
int z = eingabeStrom.read();
if( z == -1 ) return -1;
switch( (char)z ) {
case '\u00C4': puffer = 'e'; return 'A';
case '\u00D6‚: puffer = 'e'; return 'O';
case '\u00DC': puffer = 'e'; return 'U';
case '\u00E4': puffer = 'e'; return 'a';
case '\u00F6': puffer = 'e'; return 'o';
case '\u00FC': puffer = 'e'; return 'u';
case '\u00DF': puffer = 's'; return 's';
default: return z;
} } } }
08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 696
Folgendes Programm zeigt Zusammenbau und Anwendung von Strömen:
public class Main {
public static void main(String[] args) throws IOException { String s = new String(
"\u00C4neas opfert den G\u00D6ttern "
+ "edle \u00D6le,\nauf "
+ "da\u00DF \u00FCberall "
+ "das \u00DCbel sich \u00E4ndert.");
CharEingabeStrom cs;
cs = new StringLeser( s );
cs = new UmlautSzFilter( cs );
cs = new GrossBuchstabenFilter( cs );
int z = cs.read();
while( z != -1 ) {
System.out.print( (char)z );
z = cs.read();
}
System.out.println("");
} }
Adaption von Strömen:
Adaption bedeutet in der Objektorientierung meist das Anpassen einer Schnittstelle an die Bedürfnisse eines Anwenders.
Als kleines Beispiel einer Adaption betrachten wir die typmäßige Anpassung der Klasse FileReader aus java.io an CharEingabeStrom:
FileReader CharEingabeStrom
DateiLeser
Da FileReader eine Methode read mit der gleichen Signatur und Bedeutung wie in CharEingabeStrom bereitstellt, reicht folgende fast triviale Adaptionsklasse:
public class DateiLeser
extends FileReader
implements CharEingabeStrom { public DateiLeser( String s )
throws IOException { super(s);
} }
Javas Stromklassen
Stromklassen werden nach den Datentypen, die sie verarbeiten, und ihre Quellen bzw. Senken klassifiziert.
Stromklassen sind wichtige programmiertechnische Hilfsmittel und ihre Hierarchien ein gutes Beispiel für eigenständige Bausteine.
Die Reader-/Writer-Klassen aus dem Paket java.io verarbeiten char-Ströme; die Input-/Output-Strom- klassen verarbeiten byte-Ströme.
Die Reader-Klassen unterstützen:
• das Lesen einzelner Zeichen: int read() ;
• das Lesen mehrerer Zeichen aus der Quelle und Ablage in ein char-Feld: int read(char[]) u. ä.;
• das Überspringen einer Anzahl von Zeichen der Eingabe: long skip(long) ;
• die Abfrage, ob der Strom für das Lesen des nächsten Zeichens bereit ist ;
• das Schließen des Eingabestroms: void close();
• Methoden zum Markieren und Zurücksetzen des Stroms.
08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 699
Die Writer-Klassen unterstützen:
• das Schreiben einzelner Zeichen:
void write( int ) ;
• das Schreiben mehrerer Zeichen eines char- Feldes: void write(char[]) u. ä.;
• das Schreiben mehrerer Zeichen eines String:
void write(String) u. ä.;
• die Ausgabe ggf. im Strom gepufferter Zeichen:
void flush() ;
• das Schließen des Ausgabestroms: void close().
Die genannten Methoden lösen möglicherweise eine IOException aus.
Die vonInputStreambzw.OutputStreamabge- leiteten Klassen leisten Entsprechendes für Daten vom Typ byte.
Reader-/Writer-Klassen:
Die Reader-Klassen unterscheiden sich im Wesentlichen durch ihre Quelle:
08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 700
Reader-Klasse Quelle Bemerkung InputStreamReader InputStream
FileReader byte-Strom aus Datei
BufferedReader Reader puffernd; kann LineNumberReader Reader zeilenweise lesen PipedReader PipedWriter
FilterReader Reader
PushBackReader Reader Methodeunread CharArrayReader char[]
StringReader String
InputStreamR PipedR
Reader
CharArrayR StringR
BufferedR FilterR
FileR LineNumberR PushBackR
Bemerkung:
Die Konstruktoren ermöglichen das Zusammenhängen von Strömen; hier am Beispiel eines Konstruktors der Klasse PrintWriter:
public PrintWriter(OutputStream o,boolean af){
this(new BufferedWriter(
new OutputStreamWriter(o)), af);
}
Writer arbeiten entsprechend den Reader-Klassen, nur in umgekehrter Richtung.
PrintWriter unterstützen die formatierte Ausgabe von Daten durch die Methoden print und println, die alle Standarddatentypen als Parameter nehmen.
Writer
OutputStreamW PipedW CharArrayW StringW
BufferedW FilterW PrintW
FileW
Beispiel: (Reader-/Writer-Klassen)
public class DateiZugriff { public static
String lesen( String dateiname )
throws FileNotFoundException, IOException {
BufferedReader in = new BufferedReader(
new FileReader( dateiname ) );
String line, inputstr = "";
line = in.readLine();
while( line != null ){
inputstr = inputstr.concat( line+"\n");
line = in.readLine();
}
in.close();
return inputstr;
}
public static
void schreiben(String dateiname,String s) throws IOException {
PrintWriter out;
out = new PrintWriter(
new FileWriter( dateiname ) );
out.print( s );
out.close();
} }
08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 703
public class DateiZugriffTest {
public static void main( String[] argf ){
String s;
try {
s = DateiZugriff.lesen( argf[0] );
} catch( FileNotFoundException e ){
System.out.println(
"Can't open "+ argf[0] );
return;
} catch( IOException e ){
System.out.println(
"IOException reading "+ argf[0] );
return;
} try {
DateiZugriff.schreiben("ausgabeDatei",s);
} catch( IOException e ){
System.out.println(
"Can't open "+ argf[0] );
} } }
08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 704
Input-/Outputstream-Klassen:
InputStream
FileIS PipedIS ByteArrayIS ObjectIS
DataInput
ObjectInput
SequenceIS FilterIS StringBufferIS
BufferedIS CheckedIS DigestIS DataIS
RandomAccess- File
DataOutput
ObjectOutput InflaterIs
LineNumberIS PushbackIS
ZipIS GZIPIS
OutputStream
FileOS PipedOS ByteArrayOS ObjectOS
FilterOS
BufferedOS CheckedOS DigestOS DataOS
InflaterOS
ZipOS GZIPOS
PrintStream
Objektströme Bemerkung:
Die Unterscheidung in Reader/Writer einerseits und Input-/Output-Ströme andererseits wäre überflüssig, wenn Java parametrische Klassen- deklarationen unterstützen würde, bei denen die Typparameter durch elementare Datentypen instanziert werden können.
Das Lesen und Schreiben von den Werten der elementaren Datentypen ist relativ einfach. Sie besitzen eindeutige Repräsentationen.
Die Ein- und Ausgabe von Objekten ist komplexer:
- Der Zustand reicht zur Repräsentation eines Objektes nicht aus.
- Objektreferenzen besitzen nur innerhalb des aktuellen Prozesses eine Gültigkeit.
- Bei Objekten ist häufig ihre Rolle im Objekt- geflecht von entscheidender Bedeutung.
Andererseits ist Ein- und Ausgabe von Objekten wichtig, um
- Objekte zwischen Prozessen auszutauschen;
- Objekte für nachfolgende Programmläufe zu speichern, d.h. persistent zu machen.
Beispiel: (Objekte: Wie ausgeben?)
LinkedList ll = new LinkedList();
StringBuffer s = new StringBuffer("Sand");
ll.add("Sich ");
ll.add("mit ");
ll.add(s);
ll.add("alen ");
ll.add("im ");
ll.add(s);
ll.add(" aalen");
Was bedeutet es, das von ll referenzierte Objekt auszugeben(?):
- nur das LinkedList-Objekt ausgeben;
- das LinkedList-Objekt und die zugehörigen Entry- Objekte ausgeben;
- das LinkedList-Objekt, die zugehörigen Entry-Objekte sowie die String-Objekte und das StringBuffer-Objekt ausgeben.
08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 707
Um Objekte in ihrem Zusammenwirken mit anderen Objekten wieder einlesen zu können, müssen sie gemeinsam mit allen erreichbaren Objekten ausgegeben werden.
Dabei bekommen sie eine Kennung, die relativ zu den anderen Objekten des Geflechts eindeutig ist.
Wg. möglicher Zyklen ist die Implementierung der Ausgabe und des Einlesens von Geflechten nicht einfach. Darum gibt es dafür eine Unterstützung in der Bibliothek.
Beispiel: (Ausgabe von Objektgeflechten)
Sei die Variable llwie in obigem Beispiel:
OutputStream os =
new FileOutputStream("speicherndeDatei");
ObjectOutputStream oos =
new ObjectOutputStream( os );
oos.writeObject( ll );
Der Methodenaufruf in der letzten Zeile führt zur Ausgabe aller vonllaus erreichbaren Objekte in die Datei mit Namen „speicherndeDatei“.
08.02.2007 © A. Poetzsch-Heffter, TU Kaiserslautern 708
Das Einlesen von Objekten und den mit ihnen abgelegten erreichbaren Objekten birgt eine weitere Schwierigkeit:
Beim Einlesen müssen Objekte erzeugt werden.
Dafür müssen alle Klassen der einzulesenden Objekte und geeignete Konstruktoren zur Verfügung stehen. (Zum Auffinden benutzt Java die Mechanismen der Reflexion.)
Beispiel: (Einlesen von Objektgeflechten)
Der Methodenaufruf ois.readObject() liest ein Objektgeflecht aus der Datei mit Namen
„speicherndeDatei“ ein.
LinkedList inll;
InputStream is =
new FileInputStream("speicherndeDatei");
ObjectInputStream ois =
new ObjectInputStream( is );
try {
inll = (LinkedList)ois.readObject();
} catch( ClassNotFoundException exc ) {
System.out.println("Klasse zu Objekt fehlt");
}
Zur Beachtung:
• Gibt man ein Objekt mit den erreichbaren Objekten aus und liest es wieder ein, entsteht eine Kopie.
• Referenziert man von mehreren Variablen Teile des gleichen Geflechts, kommt es beim Einlesen ggf.
zu mehreren Kopien eines Objekts des ursprüng- lichen Geflechts.
Beispiel: (Ausgabe u. Einlesen von Objekten)
Vor Ausgabe: Nach Einlesen:
Ausgabe und Einlesen der von undreferenzierten Objekte und Geflechte:
o1:S
a: b: c: a: b: c:
o2:V o11:S o21:V
o3:T
204 204
o4:T o13:T o14:T o4:T o22:T
o5:U true
o15:U true
o5:U true
o23:U true
Begriffsklärung: (Serialisieren)
Serialisieren bedeutet alle von einem Objekt aus erreichbaren Objekte der Reihe nach in kodierter Form in einen Strom zu schreiben.
Deserialisieren bezeichnet den umgekehrten Prozess.
Bemerkung:
• Serialisieren hat zwei zentrale Anwendungen:
- Persistenz von Objekten zu unterstützen;
- Parameterübergabe bei der verteilten objekt- orientierten Programmen zu realisieren.
• Der Serialisierungsmechanismus muss im Allg.
Zugriff auf private Daten haben und adaptierbar sein.
• In Java wird die Serialisierbarkeit der Objekte einer Klasse K dadurch ausgedrückt, dass K die Schnittstelle Serializable implementiert.