5.4 Objektorientierte Bausteine und Bibliotheken Dieser Abschnitt führt in den Komponenten-/ Bausteinbegriff und in die Java-Bibliothek ein. Ziel ist die Vertiefung der in 5.3 erläuterten Konzepte und deren Anwendung. Überblick: • Bausteine, Schnittstellen und Bibliotheken • Klassen zur Ausnahmebehandlung • Ströme zur Ein- und Ausgabe 5.4.1 Bausteine, Schnittstellen und Bibliotheken • Klärung und Diskussion der Begriffe - Programmbaustein, -komponente - API, Programmierschnittstelle - Bibliothek • Überblick über die Java-Bibliothek 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 211 Programmbausteine & Komponenten Wir verwenden die Begriffe „Programmbaustein“ und „Programmkomponente“ hier synonym. Idee: Konstruiere Softwaresysteme aus vorgefertigten Bausteinen durch • Anpassen der Bausteine: - Instanzieren von Parametern - Spezialisierung • Verbinden der Bausteine/Komposition: - Verbindungsmechanismen der Komponenten - zusätzlicher Programmcode (engl. glue code) Wichtige Fragestellungen: - Wie anpassbar, wie allgemein sind die Bausteine? - Sind die Bausteine direkt einsetzbar oder müssen sie noch vervollständigt werden? - Sind die Bausteine unabhängig voneinander, hierarchisch strukturiert, wechselseitig abhängig? - Wie werden sie zusammengesetzt? - Wie kann man sie auffinden? 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 212 Vereinfachend gehen wir davon aus, dass ein Baustein durch eine oder mehrere Typdeklarationen realisiert ist. Die Schnittstelle eines Bausteins entspricht der öffentlichen Schnittstelle dieser Typen. Wir betrachten drei Arten von Bausteinen: - unabhängige Bausteine - eigenständige Bausteine - kooperierende Bausteine Wir erläutern diese Aufteilung jeweils durch Begriffsklärung und Beispiele in Java. Begriffsklärung: (unabhängige Bausteine) Wir nennen Bausteine oder Programmkomponenten unabhängig, wenn sie ohne Kenntnis und Existenz anderer Bausteine bzw. Komponenten angewandt werden können. In der Schnittstelle unabhängiger Bausteine kommen im Wesentlichen nur vordefinierte und Standardtypen vor. Typische Beispiele sind einfache Behälterbausteine (z.B. SLinkedList), die Klasse String oder die Wrapper-Klassen, aber auch vollständige Anwendungsprogramme. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 213 Beispiel: (unabhängiger „Baustein“) public final class Boolean implements java.io.Serializable { public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false); public static final Class TYPE = ...; public Boolean(boolean value) { this.value = value; } public Boolean(String s) { this( toBoolean(s) ); } public boolean booleanValue(){ return value; } public static Boolean valueOf(String s){...} public String toString() { ... } public int hashCode() { ... } public boolean equals(Object obj) { if (obj instanceof Boolean) { return value == ((Boolean)obj).booleanValue(); } return false; } public static boolean getBoolean(String n){..} } 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 214 Unabhängigkeit erleichtert das Verständnis von Bausteinen. Trotzdem sind unabhängige Bausteine in Programmbibliotheken eher selten. Häufig sind unterschiedliche, aber ähnliche Bausteine Teil einer Klassen- bzw. Typhierarchie. Dadurch sind speziellere Bausteine oft von allgemeineren abhängig. Begriffsklärung: (eigenständige Bausteine) Wir nennen Bausteine oder Programmkomponenten eigenständig, wenn die Kenntnis und Existenz ihrer Supertypen für ihr Verständnis und ihre Anwendung ausreicht und sie eingesetzt werden können, ohne mit anderen Bausteinen kooperieren zu müssen. In der Schnittstelle eigenständiger Bausteine kommen im Wesentlichen nur vordefinierte Typen, Standardtypen und Supertypen der Klassen vor, die den Baustein implementieren. Typische Beispiele sind komplexere Behälterbausteine und Stromklassen. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 215 Beispiel: (eigenstängiger Baustein) Listen mit Iteratoren bilden einen Baustein. Als Beispiel betrachten wir die Typen LinkedList, Iterator und ListIterator aus dem Paket java.util. public interface Iterator { boolean hasNext(); Object next(); void remove(); } public interface ListIterator extends Iterator { boolean hasNext(); Object next(); boolean hasPrevious(); Object previous(); int nextIndex(); int previousIndex(); void remove(); void set(Object o); void add(Object o); } 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 216 public class LinkedList extends AbstractSequentialList implements List, Cloneable, Serializable { public LinkedList() public LinkedList(Collection c) public Object getFirst() public Object getLast() public Object removeFirst() public Object removeLast() public void addFirst(Object o) public void addLast(Object o) public boolean contains(Object o) public int size() public boolean add(Object o) public boolean remove(Object o) public boolean addAll(Collection c) public boolean addAll(int ix, Collection c) public void clear() public Object get(int index) public Object set(int ix, Object elem) public void add(int index, Object elem) public Object remove(int index) public int indexOf(Object o) public int lastIndexOf(Object o) // ... weiter nächste Seite 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 217 // ... public public public public public public public public public ... // ListIterator listIterator() ListIterator listIterator(int ix) Iterator iterator() List subList(int from, int to) boolean equals(Object o) int hashCode() Object clone() Object[] toArray() Object[] toArray(Object a[]) Methoden aus Object } Bemerkung: In Abschnitt 5.4.3 werden wir weitere eigenständige Bausteine kennen lernen. Im Allg. agieren Bausteine nicht eigenständig, sondern müssen mit anderen und andersartigen Bausteinen kooperieren, um ihre Funktionalität zu erbringen. Bausteine können eng oder nur lose kooperieren. Enge Kooperation findet man typischerweise in Programmgerüsten (engl. program frameworks). 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 218 Die Kooperation zwischen Bausteinen kann man syntaktisch an den (rekursiven) Abhängigkeiten der Typdeklarationen erkennen. Beispiel: (Abhängigkeiten bei Kooperation) Folgende Fragmente stammen aus dem AWT (Pakete java.awt, etc.), einem Java-Framework zur Implementierung graphischer Bedienoberflächen. class Component ... { ... void addComponentListener(ComponentListener c) ... } interface ComponentListener ... { ... void componentResized( ComponentEvent e ); ... } class ComponentEvent ... { ... Component getComponent() ... ... } 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 219 Bemerkung: • Die Grenzen eines Bausteins werden vom Bausteinentwickler festgelegt und lassen sich im Allg. nicht aus dem Programmtext erschließen. • Die Unterscheidung zwischen den Bausteinarten ist nicht scharf. Begriffsklärung: (Programmierschnittstelle) Die Programmierschnittstelle eines Bausteins (einer Komponente) besteht aus den öffentlichen Typen und Methoden, mit denen der Baustein aus Programmen heraus gesteuert werden kann. In gleicher Weise spricht man von der Programmierschnittstelle einer Anwendung oder eines Systems (engl. application programming interface, API). Beispiele: (Programmierschnittstellen) Es gibt Programmierschnittstellen für den: • Zugriff aufs Dateisystem • Zugriff auf andere Teile des Betriebssystems • Zugriff aufs Netzwerk • Zugriff auf eine Anwendung, etwa ein Spiel 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 220 Bemerkung: Nicht alles, was sich API nennt, ist in obigem Sinne eine Programmierschnittstelle. Begriffsklärung: (Programmbibliothek) Eine Programmbibliothek oder einfach Bibliothek ist eine strukturierte Ansammlung von Programmteilen, die für die Wiederverwendung entwickelt und organisiert sind. Die Programmteile können unterschiedlicher Art sein (z.B. standardisierte Bausteine der Sprache, eigenständige Bausteine, Programmierschnittstellen, Programmgerüste). Beispiele: (Programmbibliotheken) • zu Programmiersprachen gehörende Bibliotheken (z.B. Java-Bibliothek) • Bibliotheken für Datenstrukturen: STL, Leda, ... • Bibliotheken für graphische Bedienoberflächen, zu allgemeinen Anwendungsbaukästen • Bibliotheken für Graphik, CAD, virtuelle Welten, ... • Anwendungsspezifische Bibliotheken 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 221 Wichtige Pakete der Java-Bibliothek applet event awt font beans image io ref lang java math reflection net nio registry rmi server security sql text zip javax.* util jar org. * 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 222 Bemerkung: Die obige Darstellung der Java-Bibliothek ist unvollständig. Beispielsweise enthält das Paket javax.swing ein leistungsfähigeres Programmgerüst zur Realisierung graphischer Bedienoberflächen (GUI Framework). 5.4.2 Klassen zur Ausnahmebehandlung Realisierung der Ausnahmebehandlung in Java demonstriert zwei Aspekte: - erweiterbare Hierarchie einfacher Bausteine - Zusammenspiel von Sprache und Standardbibliothek Darüber hinaus ist das Verständnis der Ausnahmebehandlung von allgemeinem Interesse für die Softwareentwicklung. Klassifikation von Ausnahmesituationen Die möglichen Ausnahmesituationen werden in Java durch eine Typhierarchie klassifiziert. Alle Ausnahmeklassen sind Subklassen von java.lang.Throwable 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 223 Es gibt drei Arten von Ausnahmesituationen: 1. Nicht korrigierbare Ausnahmesituationen (Error): Drei typische Beispiele: - kein Speicherplatz mehr verfügbar zum Erzeugen von Objekten (OutOfMemoryError) - zuviele unbeendete Methodenaufrufe (StackOverflowError) - Inkonsistenzen zwischen übersetzten Klassen (z.B. AbstractMethodError) 2. Programmierfehler (RuntimeException): Typische Beispiele: - Dereferenzieren von null (NullPointerException): Object ov = null; ov.equals( new Object() ); - Feldzugriff mit unzulässigem Index (IndexOutOfBoundsException): int[] iav = new int[37]; int n = iav[333]; 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 224 3. Nicht vermeidbare, aber behandelbare Situationen: Beispiele sind der Zugriff auf eine Ressource, etwa das Netz oder eine Datei, die augenblicklich anderweitig genutzt wird. Abhilfe: - zeitverzögert nochmaligen Zugriff - Anwendung einer vergleichbaren Ressource. In diese Kategorie fallen auch die meisten anwendungsspezifischen Ausnahmen. Beispiel: (anwendungsspez. Ausnahmekl.) public class KeinKaffeeException extends Exception { private float restMenge; KeinKaffeeException( float kaffeeMenge ){ restMenge = kaffeeMenge; } public float getRestMenge() { return restMenge; } } 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 225 Insgesamt ergibt sich folgende Klassifikation bzw. Typhierarchie: Object Throwable Error Exception IOException AbstractMethodError ClassNotFoundException OutOfMemoryError KeinKaffeeException StackOverflowError RuntimeException NullPointerException IndexOutOfBoundsException IllegalArgumentException NumberFormatException 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 226 Zusammenspiel von Sprache und Bibliothek Die Ausnahmebehandlung in Java hat vier Aspekte: - Welche Ausnahmen gibt es? - Wodurch werden Ausnahmen ausgelöst? - Wie werden Ausnahmen ggf. über Methodengrenzen hinweg weitergereicht? - Wie kann man Ausnahmen abfangen und behandeln? 1. Aspekt: In Java werden Ausnahmen als Subklassen von Throwable realisiert. D.h. - vordefinierte Ausnahmen in der Standardbibliothek - benutzerdefinierte Ausnahmen als Subklassen 2. Aspekt: Fehler und Laufzeitausnahmen werden durch das Laufzeitsystem von Java (Java Virtual Machine) ausgelöst. Andere Ausnahmen durch Systemschnittstellen oder Anwenderprogramme mittels der throw-Anweisung. Der Ausdruck in der throw-Anweisung muss ein Subtyp von Throwable sein. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 227 3. Aspekt: Behandlung von Ausnahmen: - im umfassenden try-Block; - die aktuelle Methode terminiert abrupt und reicht die Ausnahme an die Aufrufstelle weiter. Der Java-Übersetzer prüft, welche Ausnahmen eine Methode möglicherweise auslöst, ohne sie abzufangen. Alle nicht-abgefangenen Ausnahmen, die weder Subklassen von Error noch von RunTimeException sind, müssen in der Signatur der Methode deklariert werden (siehe Beispiel). 4. Aspekt: Mittels der try-catch-Anweisung kann man Ausnahmen (und Fehler) abfangen: try { <Anweisungsblock1> } catch( <Subtyp von Throwable> eb ) { <Anweisungsblock2> } ... Tritt im Anweisungsblock1 eine Ausnahme vom Typ E auf und ist E ein Subtyp des Typs, der in der catchKlausel genannt ist, dann wird die Ausnahme abgefangen und der Anweisungsblock2 ausgeführt. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 228 Andernfalls wird die Ausnahme weitergereicht. Beispiel: ( Ausnahmebehandlung) public class KaffeeMaschine { private KaffeeSpeicher speicher; ... void fuellenFilter( float benoetigteMenge ) throws KeinKaffeeException { float restMenge; restMenge = speicher.messenFuellung(); if( restMenge < benoetigteMenge ) { throw new KeinKaffeeException(restMenge); } ... } } Bemerkung: • Häufig kann die Behandlung der Ausnahme erst von einem weiter außen liegenden Aufrufer erledigt werden. • Eine überschreibende Methode darf nur die Ausnahmen auslösen, die auch die überschriebene Methode auslösen darf. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 229 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.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 230 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 (F. 3.82). 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 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 231 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. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 232 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.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 233 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 case '\u00D6‚: puffer = 'e'; return case '\u00DC': puffer = 'e'; return case '\u00E4': puffer = 'e'; return case '\u00F6': puffer = 'e'; return case '\u00FC': puffer = 'e'; return case '\u00DF': puffer = 's'; return default: return z; } } } 'A'; 'O'; 'U'; 'a'; 'o'; 'u'; 's'; } 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 234 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(""); } } 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 235 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: CharEingabeStrom FileReader 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); } } 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 236 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-Stromklassen 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.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 237 Die Writer-Klassen unterstützen: • das Schreiben einzelner Zeichen: void write( int ) ; • das Schreiben mehrerer Zeichen eines charFeldes: 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 von InputStream bzw. OutputStream abgeleiteten Klassen leisten Entsprechendes für Daten vom Typ byte. Reader-/Writer-Klassen: Die Reader-Klassen unterscheiden sich im Wesentlichen durch ihre Quelle: 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 238 Reader InputStreamR PipedR BufferedR FileR CharArrayR StringR FilterR LineNumberR PushBackR Reader-Klasse Quelle InputStreamReader InputStream FileReader byte-Strom aus Datei BufferedReader Reader puffernd; kann LineNumberReader Reader zeilenweise lesen PipedReader PipedWriter FilterReader Reader PushBackReader Reader CharArrayReader char[] StringReader String 08.01.09 Bemerkung Methode unread © A. Poetzsch-Heffter, TU Kaiserslautern 239 Writer OutputStreamW PipedW BufferedW CharArrayW FilterW StringW PrintW FileW 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. 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); } 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 240 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.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 241 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.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 242 Input-/Outputstream-Klassen: DataInput InputStream ObjectInput FileIS SequenceIS BufferedIS PipedIS ByteArrayIS FilterIS CheckedIS LineNumberIS ObjectIS StringBufferIS DigestIS InflaterIs DataIS PushbackIS RandomAccessFile ZipIS GZIPIS DataOutput OutputStream ObjectOutput FileOS PipedOS ByteArrayOS ObjectOS FilterOS BufferedOS CheckedOS DigestOS InflaterOS ZipOS 08.01.09 DataOS PrintStream GZIPOS © A. Poetzsch-Heffter, TU Kaiserslautern 243 Bemerkung: Die Unterscheidung in Reader/Writer einerseits und Input-/Output-Ströme andererseits wäre überflüssig, wenn Java parametrische Klassendeklarationen unterstützen würde, bei denen die Typparameter durch elementare Datentypen instanziert werden können. Objektströme 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 Objektgeflecht von entscheidender Bedeutung. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 244 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 EntryObjekte ausgeben; - das LinkedList-Objekt, die zugehörigen Entry-Objekte sowie die String-Objekte und das StringBuffer-Objekt ausgeben. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 245 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 ll wie 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 von ll aus erreichbaren Objekte in die Datei mit Namen „speicherndeDatei“. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 246 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) 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"); } Der Methodenaufruf ois.readObject() liest ein Objektgeflecht aus der Datei mit Namen „speicherndeDatei“ ein. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 247 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ünglichen Geflechts. Beispiel: (Ausgabe u. Einlesen von Objekten) Ausgabe und Einlesen der von a und c referenzierten Objekte und Geflechte: Vor Ausgabe: a: b: Nach Einlesen: c: o1:S o2:V a: b: c: o11:S o21:V 204 o3:T o13:T o14:T o4:T o22:T o5:U o15:U o5:U o23:U true true 08.01.09 o4:T 204 true © A. Poetzsch-Heffter, TU Kaiserslautern true 248 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 objektorientierten 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. 08.01.09 © A. Poetzsch-Heffter, TU Kaiserslautern 249