22.5 Serialisierung und Persistenz In den meisten Anwendungsszenarien werden Daten nicht nur während der Laufzeit eines Programmes verwendet, sondern müssen dauerhaft (persistent) und unabhängig von der virtuellen Maschine verfügbar sein. So macht es beispielsweise wenig Sinn, in einer Debitoren- und Kreditorenverwaltung jeden Kunden bei Programmstart neu zu erfassen. Für solche Zwecke stehen eigens Datenbanksysteme zur Verfügung, die diese Informationen persistent, konsistent und effizient verwalten. Auf die Möglichkeit des Datenbankzugriffs aus Java werden wir später noch eingehen. In diesem Abschnitt befassen wir uns jedoch mit den Klassen ObjectInputStream und ObjectOutputStream, die eine einfache Möglichkeit darstellen, Objektpersistenz unabhängig von Datenbanksystemen zu erreichen. In den vorangehenden Beispielen haben wir bereits gesehen, wie Zeichen oder Bytes in Dateien geschrieben und wieder gelesen werden können. Mit den erwähnten ObjectStreams lässt sich dieses Prinzip auf beliebige Objekte verallgemeinern: Die Klasse ObjectOutputStream kann beliebige Objekte in einen Byte-Strom zerlegen (Serialisierung), der dann in eine Datei geschrieben wird. Bei der Serialisierung wird die Struktur des Objekts durchlaufen und sowohl strukturelle als auch inhaltliche Information in den Byte-Strom geschrieben (vgl. Abbildung 22.2). Bei der Deserialisierung (durch die Klasse ObjectInputStream) wird umgekehrt ein Byte-Strom gelesen und das ursprüngliche Objekt wieder rekonstruiert. Adresse String strasse; String zusatz String land; String plz; String ort; Kunde int kundenNr; String name; Adresse adr; ... 100017 SAP AG Hopp-Allee 1-12 Deutschland 69189 Walldorf ... ObjectOutputStream Abb. 22.2: Serialisierung eines Objekts Da ein Objekt selbst wieder Referenzen auf Objekte als Attribute besitzen kann, handelt es sich bei der Serialisierung und Deserialisierung um Verfahren, die rekursiv arbeiten und zyklische Abhängigkeiten sowie mehrfache Referenzen auf das gleiche Objekt berücksichtigen. Die konkrete Implementierung der Serialisierung und 371 Deserialisierungsverfahren bleiben dem Java-Entwickler verborgen, es sei denn, man studiert den Quelltext der beteiligten Klassen. Betrachten wir folgendes Beispiel, bei dem ein Zeitstempel in eine Datei geschrieben, nach kurzer Zeit aus dieser gelesen und an der Konsole ausgegeben wird; aus Gründen der Übersichtlichkeit verzichten wir hier auf eine detaillierte Ausnahmebehandlung: import java.io.*; import java.util.*; public class WriteAndReadDate { public static void main( String[] args ) throws Exception { // OutputStream erzeugen ObjectOutputStream os = new ObjectOutputStream( new FileOutputStream("l.ser") ); Date d = new Date(); // aktuellen Zeitstempel ... os.writeObject( d ); // ... in den Strom schreiben os.close(); // Strom schließen Thread.sleep( 2400 ); // eine Weile warten // InputStream erzeugen ObjectInputStream is = new ObjectInputStream( new FileInputStream("l.ser") ); Object o = is.readObject(); // Object aus dem Strom lesen is.close(); // Strom schließen System.out.println( o ); // Inhalt ausgeben // aktuellen Zeitstempel zum Vergleich ausgeben System.out.println( new Date() ); } } Man erzeugt einen ObjectOutputStream, indem dem Konstruktor eine Instanz von FileOutputStream als Argument übergeben wird. Auch hier tritt das bereits erwähnte "Decorator-Pattern" auf: ein OutputStream wird mit der zusätzlichen Fähigkeit versehen, auch Objekte zu serialisieren. Analog erhält man eine Instanz von ObjectInputStream, indem ein FileInputStream dekoriert wird. Um ein Objekt in den Strom zu schreiben, wird die Methode writeObject() aufgerufen, der man das zu schreibende Objekt übergibt. Zum Lesen verwendet man die Methode readObject(), die das gelesene Objekt zurück gibt. Grundsätzlich können mit diesem Verfahren beliebige Objekte unterschiedlicher Klassen serialisiert werden, sofern diese das Interface Serializable implementie- ren; dabei handelt es sich um ein so genanntes Marker-Interface, das weder Methoden noch Attribute besitzt und lediglich dazu dient, Serialisierbarkeit zu kennzeichnen. Attribute eines Objekts, die mit dem Schlüsselwort transient (vergänglich, kurzlebig) gekennzeichnet sind, werden von der Serialisierung ausgenommen und beim Deserialisieren mit ihrem Default-Wert belegt. Der Versuch, Instanzen von Klassen zu serialisieren, die nicht Serializable implementieren, scheitert mit einer NotSerializableException. Dies passiert auch, wenn die Klasse ein weder static noch transient gekennzeichnetes Attribut besitzt, das nicht vom Typ Serializable ist. Werden Instanzen unterschiedlicher Klassen mit dem dargestellten Verfahren in einer einzigen Datei persistent gespeichert, ist beim Deserialisieren die Reihenfolge der abgelegten Datentypen zu beachten (vgl. Übung 22.4). Das beim Serialisieren implizit festgelegte Protokoll muss eingehalten werden. Auch wenn die Begriffe Serialisierung und Persistenz häufig im gleichen Kontext verwendet werden, bezeichnen sie unterschiedliche Vorgänge: Serialisierung bedeutet das Zerlegen möglicherweise sehr komplex strukturierter Objekte in einen linearen Strom, Persistenz dagegen das dauerhafte Speichern von Objekten auf einem externen Datenträger. Persistenz wird zwar häufig dadurch erreicht, dass Objekte serialisiert in einen mit einer Datei verbundenen Strom geschrieben werden. Dies ist jedoch nicht der einzige Anwendungsbereich des Serialisierungskonzeptes. Wir werden später sehen, wie sich damit Objekte von einer Anwendung zur anderen, ja sogar über Rechnergrenzen hinweg übertragen lassen.