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