Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage |
<< | < | > | >> | API | Kapitel 42 - Serialisierung |
Unter Serialisierung wollen wir die Fähigkeit verstehen, ein Objekt, das im Hauptspeicher der Anwendung existiert, in ein Format zu konvertieren, das es erlaubt, das Objekt in eine Datei zu schreiben oder über eine Netzwerkverbindung zu transportieren. Dabei wollen wir natürlich auch den umgekehrten Weg einschließen, also das Rekonstruieren eines in serialisierter Form vorliegenden Objekts in das interne Format der laufenden Java-Maschine.
Serialisierung wird häufig mit dem Begriff Persistenz gleichgesetzt, vor allem in objektorientierten Programmiersprachen. Das ist nur bedingt richtig, denn Persistenz bezeichnet genaugenommen das dauerhafte Speichern von Daten auf einem externen Datenträger, so dass sie auch nach dem Beenden des Programms erhalten bleiben. Obwohl die persistente Speicherung von Objekten sicherlich eine der Hauptanwendungen der Serialisierung ist, ist sie nicht ihre einzige. Wir werden später Anwendungen sehen, bei der die Serialisierung von Objekten nicht zum Zweck ihrer persistenten Speicherung genutzt werden. |
|
Das Paket java.io enthält die Klasse ObjectOutputStream, mit der Objekte serialisiert werden können. ObjectOutputStream besitzt einen Konstruktor, der einen OutputStream als Argument erwartet:
public ObjectOutputStream(OutputStream out) throws IOException |
java.io.ObjectOutputStream |
Der an den Konstruktor übergebene OutputStream dient als Ziel der Ausgabe. Hier kann ein beliebiges Objekt der Klasse OutputStream oder einer daraus abgeleiteten Klasse übergeben werden. Typischerweise wird ein FileOutputStream verwendet, um die serialisierten Daten in eine Datei zu schreiben.
ObjectOutputStream besitzt sowohl Methoden, um primitive Typen zu serialisieren, als auch die wichtige Methode writeObject, mit der ein komplettes Objekt serialisiert werden kann:
public final void writeObject(Object obj) throws IOException public void writeBoolean(boolean data) throws IOException public void writeByte(int data) throws IOException public void writeShort(int data) throws IOException public void writeChar(int data) throws IOException public void writeInt(int data) throws IOException public void writeLong(long data) throws IOException public void writeFloat(float data) throws IOException public void writeDouble(double data) throws IOException public void writeBytes(String data) throws IOException public void writeChars(String data) throws IOException public void writeUTF(String data) throws IOException |
java.io.ObjectOutputStream |
Während die Methoden zum Schreiben der primitiven Typen ähnlich funktionieren wie die gleichnamigen Methoden der Klasse RandomAccessFile (siehe Abschnitt 21.4), ist die Funktionsweise von writeObject wesentlich komplexer. writeObject schreibt folgende Daten in den OutputStream:
Insbesondere der letzte Punkt verdient dabei besondere Beachtung. Die Methode writeObject durchsucht also das übergebene Objekt nach Membervariablen und überprüft deren Attribute. Ist eine Membervariable vom Typ static, wird sie nicht serialisiert, denn sie gehört nicht zum Objekt, sondern zur Klasse des Objekts. Weiterhin werden alle Membervariablen ignoriert, die mit dem Schlüsselwort transient deklariert wurden. Auf diese Weise kann das Objekt Membervariablen definieren, die aufgrund ihrer Natur nicht serialisiert werden sollen oder dürfen. Wichtig ist weiterhin, dass ein Objekt nur dann mit writeObject serialisiert werden kann, wenn es das Interface Serializable implementiert.
Aufwändiger als auf den ersten Blick ersichtlich ist das Serialisieren von Objekten vor allem aus zwei Gründen:
Wir wollen uns zunächst ein Beispiel ansehen. Dazu konstruieren wir eine einfache Klasse Time, die eine Uhrzeit, bestehend aus Stunden und Minuten, kapselt:
001 /* Time.java */ 002 003 import java.io.*; 004 005 public class Time 006 implements Serializable 007 { 008 private int hour; 009 private int minute; 010 011 public Time(int hour, int minute) 012 { 013 this.hour = hour; 014 this.minute = minute; 015 } 016 017 public String toString() 018 { 019 return hour + ":" + minute; 020 } 021 } |
Time.java |
Time besitzt einen öffentlichen Konstruktor und eine toString-Methode zur Ausgabe der Uhrzeit. Die Membervariablen hour und minute wurden als private deklariert und sind nach außen nicht sichtbar. Die Sichtbarkeit einer Membervariable hat keinen Einfluss darauf, ob es von writeObject serialisiert wird oder nicht. Mit Hilfe eines Objekts vom Typ ObjectOutputStream kann ein Time-Objekt serialisiert werden:
001 /* Listing4202.java */ 002 003 import java.io.*; 004 import java.util.*; 005 006 public class Listing4202 007 { 008 public static void main(String[] args) 009 { 010 try { 011 FileOutputStream fs = new FileOutputStream("test1.ser"); 012 ObjectOutputStream os = new ObjectOutputStream(fs); 013 Time time = new Time(10,20); 014 os.writeObject(time); 015 os.close(); 016 } catch (IOException e) { 017 System.err.println(e.toString()); 018 } 019 } 020 } |
Listing4202.java |
Wir konstruieren zunächst einen FileOutputStream, der das serialisierte Objekt in die Datei test1.ser schreiben soll. Anschließend erzeugen wir einen ObjectOutputStream durch Übergabe des FileOutputStream an dessen Konstruktor. Nun wird ein Time-Objekt für die Uhrzeit 10:20 konstruiert und mit writeObject serialisiert. Nach dem Schließen des Streams steht das serialisierte Objekt in »test1.ser«.
Wichtig an der Deklaration von Time ist das Implementieren des Serializable-Interface. Zwar definiert Serializable keine Methoden, writeObject testet jedoch, ob das zu serialisierende Objekt dieses Interface implementiert. Ist das nicht der Fall, wird eine Ausnahme des Typs NotSerializableException ausgelöst. |
|
Ein ObjectOutputStream kann nicht nur ein Objekt serialisieren, sondern beliebig viele, sie werden nacheinander in den zugrunde liegenden OutputStream geschrieben. Das folgende Programm zeigt, wie zunächst ein int, dann ein String und schließlich zwei Time-Objekte serialisiert werden:
001 /* Listing4203.java */ 002 003 import java.io.*; 004 import java.util.*; 005 006 public class Listing4203 007 { 008 public static void main(String[] args) 009 { 010 try { 011 FileOutputStream fs = new FileOutputStream("test2.ser"); 012 ObjectOutputStream os = new ObjectOutputStream(fs); 013 os.writeInt(123); 014 os.writeObject("Hallo"); 015 os.writeObject(new Time(10, 30)); 016 os.writeObject(new Time(11, 25)); 017 os.close(); 018 } catch (IOException e) { 019 System.err.println(e.toString()); 020 } 021 } 022 } |
Listing4203.java |
Da ein int ein primitiver Typ ist, muss er mit writeInt serialisiert werden. Bei den übrigen Aufrufen kann writeObject verwendet werden, denn alle übergebenen Argumente sind Objekte.
Es gibt keine verbindlichen Konventionen für die Benennung von Dateien mit serialisierten Objekten. Die in den Beispielen verwendete Erweiterung .ser ist allerdings recht häufig zu finden, ebenso wie Dateierweiterungen des Typs .dat. Wenn eine Anwendung viele unterschiedliche Dateien mit serialisierten Objekten hält, kann es auch sinnvoll sein, die Namen nach dem Typ der serialisierten Objekte zu vergeben. |
|
Nachdem ein Objekt serialisiert wurde, kann es mit Hilfe der Klasse ObjectInputStream wieder rekonstruiert werden. Analog zu ObjectOutputStream gibt es Methoden zum Wiedereinlesen von primitiven Typen und eine Methode readObject, mit der ein serialisiertes Objekt wieder hergestellt werden kann:
public final Object readObject() throws OptionalDataException, ClassNotFoundException, IOException public boolean readBoolean() throws IOException public byte readByte() throws IOException public short readShort() throws IOException public char readChar() throws IOException public int readInt() throws IOException public long readLong() throws IOException public float readFloat() throws IOException public double readDouble() throws IOException public String readUTF() throws IOException |
java.io.ObjectInputStream |
Zudem besitzt die Klasse ObjectInputStream einen Konstruktor, der einen InputStream als Argument erwartet, der zum Einlesen der serialisierten Objekte verwendet wird:
public ObjectInputStream(InputStream in) |
java.io.ObjectInputStream |
Das Deserialisieren eines Objekts kann man sich stark vereinfacht aus den folgenden beiden Schritten bestehend vorstellen:
Das erzeugte Objekt hat anschließend dieselbe Struktur und denselben Zustand, den das serialisierte Objekt hatte (abgesehen von den nicht serialisierten Membervariablen des Typs static oder transient). Da der Rückgabewert von readObject vom Typ Object ist, muss das erzeugte Objekt in den tatsächlichen Typ (oder eine seiner Oberklassen) umgewandelt werden. Das folgende Programm zeigt das Deserialisieren am Beispiel des in Listing 42.2 serialisierten und in die Datei test1.ser geschriebenen Time-Objekts:
001 /* Listing4204.java */ 002 003 import java.io.*; 004 import java.util.*; 005 006 public class Listing4204 007 { 008 public static void main(String[] args) 009 { 010 try { 011 FileInputStream fs = new FileInputStream("test1.ser"); 012 ObjectInputStream is = new ObjectInputStream(fs); 013 Time time = (Time)is.readObject(); 014 System.out.println(time.toString()); 015 is.close(); 016 } catch (ClassNotFoundException e) { 017 System.err.println(e.toString()); 018 } catch (IOException e) { 019 System.err.println(e.toString()); 020 } 021 } 022 } |
Listing4204.java |
Hier wird zunächst ein FileInputStream
für die Datei test1.ser geöffnet
und an den Konstruktor des ObjectInputStream-Objekts
is übergeben. Alle lesenden
Aufrufe von is beschaffen ihre
Daten damit aus test1.ser. Jeder Aufruf
von readObject
liest immer das nächste gespeicherte Objekt aus dem Eingabestream.
Das Programm zum Deserialisieren muss also genau wissen, welche Objekttypen
in welcher Reihenfolge serialisiert wurden, um sie erfolgreich deserialisieren
zu können. In unserem Beispiel ist die Entscheidung einfach,
denn in der Eingabedatei steht nur ein einziges Time-Objekt.
readObject
deserialisiert es und liefert ein neu erzeugtes Time-Objekt,
dessen Membervariablen mit den Werten aus dem serialisierten Objekt
belegt werden. Die Ausgabe des Programms ist demnach:
10:20
Es ist wichtig zu verstehen, dass beim Deserialisieren nicht der Konstruktor des erzeugten Objekts aufgerufen wird. Lediglich bei einer serialisierbaren Klasse, die in ihrer Vererbungshierarchie Superklassen hat, die nicht das Interface Serializable implementieren, wird der parameterlose Konstruktor der nächsthöheren nichtserialisierbaren Vaterklasse aufgerufen. Da die aus der nichtserialisierbaren Vaterklasse geerbten Membervariablen nicht serialisiert werden, soll auf diese Weise sichergestellt sein, dass sie wenigstens sinnvoll initialisiert werden.
Auch eventuell vorhandene Initialisierungen einzelner Membervariablen
werden nicht ausgeführt. Wir könnten beispielsweise die
Time-Klasse aus Listing 42.1
um eine Membervariable seconds
erweitern:
Dann wäre zwar bei allen mit new konstruierten Objekten der Sekundenwert mit 11 vorbelegt. Bei Objekten, die durch Deserialisieren erzeugt wurden, bleibt er aber 0 (das ist der Standardwert eines int, siehe Tabelle 5.1), denn der Initialisierungscode wird in diesem Fall nicht ausgeführt. |
|
Beim Deserialisieren von Objekten können einige Fehler passieren. Damit ein Aufruf von readObject erfolgreich ist, müssen mehrere Kriterien erfüllt sein:
Soll beispielsweise die in Listing 42.3 erzeugte Datei test2.ser deserialisiert werden, so müssen die Aufrufe der read-Methoden in Typ und Reihenfolge denen des serialisierenden Programms entsprechen:
001 /* Listing4205.java */ 002 003 import java.io.*; 004 import java.util.*; 005 006 public class Listing4205 007 { 008 public static void main(String[] args) 009 { 010 try { 011 FileInputStream fs = new FileInputStream("test2.ser"); 012 ObjectInputStream is = new ObjectInputStream(fs); 013 System.out.println("" + is.readInt()); 014 System.out.println((String)is.readObject()); 015 Time time = (Time)is.readObject(); 016 System.out.println(time.toString()); 017 time = (Time)is.readObject(); 018 System.out.println(time.toString()); 019 is.close(); 020 } catch (ClassNotFoundException e) { 021 System.err.println(e.toString()); 022 } catch (IOException e) { 023 System.err.println(e.toString()); 024 } 025 } 026 } |
Listing4205.java |
Das Programm rekonstruiert alle serialisierten Elemente aus »test2.ser«.
Seine Ausgabe ist:
123
Hallo
10:30
11:25
Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage, Addison Wesley, Version 7.0 |
<< | < | > | >> | API | © 1998, 2011 Guido Krüger & Heiko Hansen, http://www.javabuch.de |