Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage
<< | < | > | >> | API | Kapitel 41 - Serialisierung

## 41.1 Grundlagen

- 41.1 Grundlagen
  - 41.1.1 Begriffsbestimmung
  - 41.1.2 Schreiben von Objekten
  - 41.1.3 Lesen von Objekten

### 41.1.1 Begriffsbestimmung

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 daß 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. |  |
Hinweis

### 41.1.2 Schreiben von Objekten

Während es vor dem JDK 1.1 keine einheitliche Möglichkeit gab, Objekte zu serialisieren, gibt es seither im Paket `java.io` die Klasse `ObjectOutputStream`, mit der das sehr einfach zu realisieren ist. `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 20.4), ist die Funktionsweise von `writeObject` wesentlich komplexer. `writeObject` schreibt folgende Daten in den `OutputStream`:

- Die Klasse des als Argument übergebenen Objekts
- Die Signatur der Klasse
- Alle nicht-statischen, nicht-transienten Membervariablen des übergebenen Objekts inkl. der aus allen Vaterklassen geerbten Membervariablen

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 es nicht serialisiert, denn es 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, daß ein Objekt nur dann mit `writeObject` serialisiert werden kann, wenn es das Interface `Serializable` implementiert.

Aufwendiger als auf den ersten Blick ersichtlich ist das Serialisieren von Objekten vor allem aus zwei Gründen:

- Erstens muß zur Laufzeit ermittelt werden, welche Membervariablen das zu serialisierende Objekt besitzt (bzw. geerbt hat) und welche von ihnen serialisiert werden sollen. Dazu wird das Reflection-API verwendet (siehe Kapitel 43).
- Zweitens kann eine Membervariable natürlich selbst ein Objekt sein, das seinerseits serialisiert werden muß. Insbesondere muß `writeObject` dabei korrekt mit zyklischen Verweisen umgehen und dafür sorgen, daß Objektreferenzen erhalten bleiben (siehe Abschnitt 41.2.3).

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

Listing 41.1: Eine serialisierbare Uhrzeitklasse

`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 Einfluß darauf, ob es von `writeObject` serialisiert wird oder nicht. Mit Hilfe eines Objekts vom Typ `ObjectOutputStream` kann ein `Time`-Objekt serialisiert werden:

    001 /* Listing4102.java */
    002
    003 import java.io.*;
    004 import java.util.*;
    005
    006 public class Listing4102
    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 }
| Listing4102.java

Listing 41.2: Serialisieren eines Time-Objekts

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`-Interfaces. 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. |  |
Hinweis

Ein `ObjectOutputStream` kann nicht nur ein Objekt serialisieren, sondern beliebig viele, sie werden nacheinander in den zugrundeliegenden `OutputStream` geschrieben. Das folgende Programm zeigt, wie zunächst ein `int`, dann ein `String` und schließlich zwei `Time`-Objekte serialisiert werden:

    001 /* Listing4103.java */
    002
    003 import java.io.*;
    004 import java.util.*;
    005
    006 public class Listing4103
    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 }
| Listing4103.java

Listing 41.3: Serialisieren mehrerer Objekte

Da ein `int` ein primitiver Typ ist, muß 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. |  |
Hinweis

### 41.1.3 Lesen von Objekten

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 Objektes kann man sich stark vereinfacht aus den folgenden beiden Schritten bestehend vorstellen:

- Zunächst wird ein neues Objekt des zu deserialisierenden Typs angelegt, und die Membervariablen werden mit Default-Werten vorbelegt. Zudem wird der Default-Konstruktor der ersten nicht-serialisierbaren Superklasse aufgerufen.
- Anschließend werden die serialisierten Daten gelesen und den entprechenden Membervariablen des angelegten Objekts zugewiesen.

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, muß 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 41.2 serialisierten und in die Datei test1.ser geschriebenen `Time`-Objekts:

    001 /* Listing4104.java */
    002
    003 import java.io.*;
    004 import java.util.*;
    005
    006 public class Listing4104
    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 }
| Listing4104.java

Listing 41.4: Deserialisieren eines Time-Objekts

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 muß 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, daß 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 nicht-serialisierbaren Vaterklasse aufgerufen. Da die aus der nicht-serialisierbaren Vaterklasse geerbten Membervariablen nicht serialisiert werden, soll auf diese Weise sichergestellt sein, daß sie wenigstens sinnvoll initialisiert werden. Auch eventuell vorhandene Initialisierungen einzelner Membervariablen werden nicht ausgeführt. Wir könnten beispielsweise die `Time`-Klasse aus Listing 41.1 um eine Membervariable `seconds` erweitern:
    private transient int seconds = 11;
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 4.1), denn der Initialisierungscode wird in diesem Fall nicht ausgeführt. |  |
Warnung

Beim Deserialisieren von Objekten können einige Fehler passieren. Damit ein Aufruf von `readObject` erfolgreich ist, müssen mehrere Kriterien erfüllt sein:

- Das nächste Element des Eingabestreams ist tatsächlich ein Objekt (kein primitiver Typ).
- Das Objekt muß sich vollständig und fehlerfrei aus der Eingabedatei lesen lassen.
- Es muß eine Konvertierung auf den gewünschten Typ erlauben, also entweder zu derselben oder einer daraus abgeleiteten Klasse gehören.
- Der Bytecode für die Klasse des zu deserialisierenden Objekts muß vorhanden sein. Er wird beim Serialisieren nicht mitgespeichert, sondern muß dem Empfängerprogramm wie üblich als kompilierter Bytecode zur Verfügung stehen.
- Die Klasseninformation des serialisierten Objekts und die im deserialisierenden Programm als Bytecode vorhandene Klasse müssen zueinander kompatibel sein. Wir werden auf diesen Aspekt in Abschnitt 41.2.1 detailliert eingehen.

Soll beispielsweise die in Listing 41.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 /* Listing4105.java */
    002
    003 import java.io.*;
    004 import java.util.*;
    005
    006 public class Listing4105
    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 }
| Listing4105.java

Listing 41.5: Deserialisieren mehrerer Elemente

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, 3. Auflage, Addison Wesley, Version 3.0
<< | < | > | >> | API | © 1998-2002 Guido Krüger, http://www.javabuch.de