Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung
 <<    <     >    >>  Kapitel 41 - Serialisierung

41.1 Grundlagen



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:

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:

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:

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:

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   API  Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0
 <<    <     >    >>  © 2000 Guido Krüger, http://www.gkrueger.com