Protokoll Stunde 6

Werbung
Praktikum aus Softwareentwicklung 2, Stunde 6
Lehrziele/Inhalt
1. Streams
2. Serialisierung
3. Netzwerkprogrammierung
Streams
In Java sind die Aufgaben Lesen und Schreiben von Datenströmen getrennt. Weiters unterscheidet
Java in Byte- und Character-Ströme. Wobei Byte-Ströme über InputStreams und OutputStreams
abstrahiert werden und Character-Ströme mit Readern und Writern. Abbildung 1 zeigt wie Daten in
einem Java-Programm gelesen und geschrieben werden können.
InputStream,
Reader
Programm
Daten
quelle
OutputStream,
Writer
Daten
senke
Abbildung 1) Lesen und Schreiben von Daten über Datenströme
Lesen von Byte-Strömen
Die abstrakte Klasse InputStream im Paket java.io ist die Basis aller Eingabeströme. Will man eine
eigene Datenquelle in Java einbinden muss man InputStream beerben und zumindest die Methode
int read() überschreiben. Alle anderen in der Klasse vorhandenen Methoden bauen auf int read() auf.
Die Methode muss pro Aufruf ein Byte von der Datenquelle liefern, der Rückgabewert ist ein int,
damit man den Wert -1 liefern kann, wenn der Datenstrom das Ende erreicht hat.
Wichtige Ableitungen von InputStream sind: FileInputSteam (lesen einer Datei),
ByteArrayInputStream (lesen aus einem Byte-Array), PipedInputStream (Kommunikation zwischen
Threads), ObjectInputStream (lesen von primitiven Datentypen und Objekten) und FilterInputStream
(Basis aller Dekoratoren für Eingabeströmen, zB für gepuffertes Lesen).
Schreiben auf Byte-Ströme
OutputStream im Paket java.io ist die Basisklasse aller Ausgabeströme. Will man eine eigene
Datensenke in Java einbinden muss man OutputStream beerben und zumindest die Methode
write(int) implementieren. Alle anderen Methoden in der Klasse bauen auf write(int) auf. Write hat
aus Symmetriegründen zu int read() einen int-Parameter, schreibt aber nur das niederwertigste Byte
in die Datensenke und ignoriert die restlichen drei Bytes.
© Markus Löberbauer 2010
Seite 17
Wichtige Ableitungen von OutputStream sind: FileOutputStream (schreiben in eine Datei),
ByteArrayOutputStream (schreiben in ein Byte-Array), PipedOutputStream (kommunizieren mit
einem anderen Thread), ObjectOutputStream (schreiben von primitiven Datentypen und Objekten)
und FilterOutputStream (Basis aller Dekoratoren für Ausgabeströme, zB für gepuffertes Schreiben).
Lesen von Character-Strömen
Die Basis aller Character-Eingabeströme ist Reader, Ableitungen von Reader müssen zumindest die
Methoden close() und int read(char[] cbuf, int off, int len) implementieren. Die Methode füllt cbuf, ab
Position off, für maximal len Zeichen; und liefert die Anzahl der tatsächlich gelieferten Zeichen
zurück.
Wichtige Ableitungen von Reader sind: InputStreamReader (lesen von einem InputStream) mit der
Unterklasse FileReader (Komfort-Klasse, lesen von einer Datei), BufferedReader (gepuffertes Lesen),
CharArrayReader und StringReader (lesen von einem Char-Array bzw. String), PipedReader
(Kommunikation zwischen Threads).
Schreiben von Character-Strömen
Die Basis aller Character-Ausgabeströme ist Writer, Ableitungen von Writer müssen zumindest die
Methoden close(), flush() und int read(char[] cbuf, int off, int len) überschreiben. Die Methode
schreibt len Zeichen von cbuf ab Position off in die Datensenke.
Wichtige Ableitungen von Writer sind: OutputStreamWriter (schreiben in einen OutputStream) mit
der Unterklasse FileWriter (Komfort-Klasse, schreiben in eine Datei), BufferedWriter (gepuffertes
Schreiben), CharArrayWriter und StringWriter (schreiben in einen Char-Array bzw. StringBuffer),
PipedWriter (Kommunikation zwischen Threads).
Standardströme
Programme haben die Standardströme: Standard-Ausgabe-Strom, Standard-Error-Strom und
Standard-Eingabe-Strom. Diese kann man über die statischen Felder System.out, System.err bzw.
System.in abrufen.
Muster Ressourcen und Exceptions
Klassen die mit Betriebssystem-Ressourcen arbeiten, müssen nach der Verwendung diese wieder
freigeben. Das trifft auf Ströme, die zum Beispiel auf Dateien arbeiten, ebenfalls zu. Um das sicher zu
stellen soll man das Muster in Abbildung 2 verwenden.
© Markus Löberbauer 2010
Seite 18
FileInputStream fis = null;
try {
fis = new FileInputStream(
"test.txt");
int c;
while ((c = fis.read()) != -1) {
char ch = (char) c;
...
}
}
catch (IOException ioex) {
...
}
finally {
if (fis != null) {
try { fis.close(); }
catch {
/* log exception */
...
}
}
}
1. Variable deklarieren, mit null
initialisieren
2. try-Block öffnen
1. Resource anlegen
2. Resource nutzen
3. catch-Block (optional)
4. finally-Block
1. Resource freigeben
Abbildung 2) Muster: Ressources und Exceptions
Dekorieren von Datenströmen mit Filter-Streams
Das Entwurfsmuster Decorator wird verwendet, um Klassen mit zusätzlichen Funktionen
auszustatten. In Java wird das Decorator-Muster eingesetzt, um Ein- und Ausgabeströme mit
zusätzlichen Funktionen zu versehen, zB Puffern, Verschlüsseln oder Komprimieren. Abbildung 3
zeigt schematisch wie Filter-Streams verwendet werden können.
Input
Stream
Filter
Input
Stream
Filter
Input
Stream
Programm
Daten
quelle
Filter
Filter
Input
Input
Input
Stream
Stream Stream
Daten
senke
Abbildung 3) Schematische Darstellung von Filter-Streams
Serialisierung
Über Serialisierung kann man Objekte in Bytes verwandeln und Objekte aus Bytes aufbauen. In Java
kann man das über ObjectOutputStream bzw. ObjectInputStream machen.
© Markus Löberbauer 2010
Seite 19
Java ist in der Lage alle primitiven Datentypen und beliebige Objekte zu serialisieren, allerdings
müssen Klassen mit dem Marker-Interface Serializable markiert werden. Wird ein solches Objekt
serialisiert, dann serialisiert Java auch alle Objekte mit, die über Felder erreichbar sind (transitive
Hülle). Zeigt ein Feld auf ein Objekt welches nicht Serializable implementiert wirft Java eine
Exception. Felder die man bei der Serialisierung auslassen möchte muss man mit transient
markieren.
Klassen können sich über die Zeit ändern, damit Änderungen in Klassen nicht zu korrupten
Datenmodellen beim deserialisieren führen kann man eine Versionsnummer als Konstante mit dem
Namen serialVersionUID in der Klasse ablegen.
Benutzerdefinierte Serialisierung
Will man mehr Einfluss auf die Serialisierung nehmen kann man die Methoden writeObject,
readObject und readObjectNoData; writerReplace und readResolve implementieren. Eine genaue
Beschreibung dieser Methoden ist in der JavaDoc des Interfaces Serializable vorhanden.
Über private void writeObject(ObjectOutputStream) kann man den Zustand eines Objekts speichern,
dabei wird der Zustand der Basisklasse automatisch gespeichert. Mit private void
readObject(ObjectInputStream) kann man den Zustand des Objekts wiederherstellen. Mit private
void readObjectNoData() kann man einen Standard-Zustand herstellen wenn keine Daten für das
Objekt vorhanden sind. Das kann passieren wenn der Datenstrom beschädigt ist oder der
Datenstrom mit einer anderen Version des Objekts geschrieben wurde.
Über die Methode ANY-ACCESS-MODIFIER Object writeReplace() kann man ein Stellvertreterobjekt
liefern, das anstelle des eigentlichen Objekts serialisiert werden soll. Mit der Methode ANY-ACCESSMODIFIER Object readResolve() kann man beim deserialisieren das ursprüngliche Objekt wieder
liefern.
Externalisieren
Will man noch mehr Einfluss auf die Serialisierung nehmen kann man das Interface Externalizable mit
den Methoden writeExternal und readExternal implementieren. Implementiert eine Klasse
Externalizable, dann sichert Java nur eine Id für das Objekt, um die Daten des Objekts und die Daten
der Superklassen muss sich der Programmierer selbst kümmern. Externalizable implementiert man
nur in Ausnahmefällen, eingeführt wurde es um die teure (und damals noch teurere) Reflection aus
dem Serialisierungsprozess herausoptimieren zu können. Das kann notwendig sein wenn man sehr
viele Objekte serialisieren muss, zB bei Methodenaufrufen über das Netzwerk.
Netzwerkprogrammierung
Will man Programme schreiben die auf unterschiedlichen Rechnern laufen und miteinander
kommunizieren müssen muss man folgende Fragen klären: Wie finden sich die verteilten
Programme? Wie wird die Verbindung aufgebaut? Wie werden Daten ausgetauscht? Diese Fragen
werden in Java durch zwei Modelle abgedeckt, dem Socket-Streaming und dem Remoting.
Socket-Streaming
Sockets sind eine Programmierschnittstelle für stream-basierte Kommunikation. In Java wird SocketStreaming über die Klassen Socket und ServerSocket umgesetzt. In diesem Modell finden sich
© Markus Löberbauer 2010
Seite 20
Programme über IP-Adressen und Ports. Java unterstützt IP Version 4 (RFC 790 u.a.) und IP Version 6
(RFC 2373 u.a.). Die Kommunikation zwischen den Rechnern erfolgt über Ein- und Ausgabeströme.
Clients bauen die Verbindung über die Klasse Socket auf, von diesem Socket kann man über die
Methode getInputStream den Eingabestrom zum Lesen und über getOutputStream den
Ausgabestrom zum Schreiben abrufen.
Server öffnen einen Port über ServerSocket, auf den sich Clients verbinden können. Am Server wird
ein Client über die Methode accept angenommen, accept liefert einen Socket zurück über den die
Kommunikation abgewickelt werden kann. Häufig wird die Client-Anfrage in einem eigenen Thread
abgearbeitet, damit mehrere Clients gleichzeitig bedient werden können.
© Markus Löberbauer 2010
Seite 21
Herunterladen