Grundlagen:
Serialisierung bedeutet, dass ein Objekt, welches in der Anwendung existiert, in ein Format konvertiert wird, das es erlaubt das Objekt anschließend in eine Datei zu speichern oder um es über eine Netzwerkverbindung zu übertragen.
Der Begriff Persistenz wird häufig mit dem Begriff Serialisierung gleichgesetzt. Persistenz bedeutet aber genaugenommen das dauerhafte Speichern von Daten auf einem externen Datenträger. Das ist zwar die Hauptanwendung der Serialisierung, sie hat aber noch weitere Aufgaben, die später behandelt werden.
Schreiben von Daten (Serialisieren):
Seit der JDK 1.1 gibt es die Möglichkeit Objekte mit der Klasse ObjectOutputStream zu Serialisieren. Dem Konstruktor dieser Klasse muss ein OutputStream übergeben werden.
Diese Klasse besitzt Methoden zur Serialisierung von primitiven Typen und die Methode writeObject zum Serialisieren eines kompletten Objekts.
public final void writeObject(Object obj) java.io.ObjectOutputStream
public void writeBoolean(boolean data)
public void writeByte(int data)
public void writeShort(int data)
public void writeChar(int data)
public void writeInt(int data)
public void writeLong(long data)
public void writeFloat(float data)
public void writeDouble(double data)
public void writeBytes(String data)
public void writeChars(String data)
public void writeUTF(String data)
Alle diese Methoden können eine IOException zurückliefern.
2.1) Serialisieren von Objekten:
Damit man ein Objekt serialisieren kann, muss es das Interface Serializable implementieren. Dieses Interface definiert zwar keine Methoden, aber es muss implementiert werden sonst wird von writeObject eine Ausnahme des Typs NotSerializableException ausgelöst.
Die verwendete Methode writeObject schreibt folgende Informationen in den OutputStream:
Die Klasse des zu serialisierenden Objekts
Die Signatur der Klasse
Alle Membervariablen des Objekts inklusive aller geerbten Attribute ausgenommen:
statische Attribute: Da diese nicht zum Objekt sondern zur Klasse des Objekts gehören.
transiente Attribute: Mit Hilfe des Schlüsselworts transient kann man Attribute definieren die nicht serialisiert werden sollen oder dürfen. Genauere Informationen siehe Punkt 4.2.
Problematisch beim Serialisieren von Objekten sind vor allem zwei Dinge:
Es muß zur Laufzeit ermittelt werden welche Membervariablen ein Objekt besitzt und welche serialisiert werden müssen.
Eine Membervariable kann ein Objekt sein, welches dann auch serialisiert werden muss. writeObject muss hierbei mit zyklischen Verweisen umgehen können und die Objektreferenzen müssen erhalten bleiben.
Lesen von Daten (Deserialisieren):
Das Deserialisieren erfolgt mit Hilfe der Klasse ObjectInputStream. Dieser muss bei der Konstruktion ein InputStream übergeben werden. Analog zum ObjectOutputStream gibt es Methoden zum Lesen der primitiven Datentypen und die Methode readObject zum Wiederherstellen eines serialisierten Objekts.
public boolean readBoolean() java.io.ObjectInputStream
public byte readByte()
public short readShort()
public char readChar()
public int readInt()
public long readLong()
public float readFloat()
public double readDouble()
public String readUTF()
public final Object readObject()
throws OptionalDataException,
throws ClassNotFoundException,
Alle diese Methoden können eine IOException zurückliefern.
Das Deserialisieren eines Objekts kann man sich vereinfacht in zwei Schritten vorstellen:
Als erstes wird ein neues Objekt des zu deserialisierenden Typs angelegt und die Membervariablen werden mit Standardwerten belegt. Weiters wird der Default Konstruktor der ersten nicht serialisierbaren Superklasse aufgerufen.
Nun werden die Daten eingelesen und diese werden dem neu erstellten Objekt zugewiesen. Jetzt hat das Objekt dieselbe Struktur und denselben Zustand wie das Objekt vor dem Serialisieren. Ausgenommen sind natürlich statische und transiente Variablen.
Beim Deserialisieren von Objekten müssen einige Bedingungen erfüllt sein:
Das nächste Element des Eingabestreams ist ein Objekt und kein primitiver Typ.
Das Einlesen aus der Datei muss fehlerfrei funktionieren.
Das eingelesene Objekt muss eine Konvertierung auf den erwünschten Typ zulassen sonst wird eine Exception ausgelöst.
Der Bytecode der benötigten Klasse muss dem Empfängerprogramm bekannt sein, da dieser nicht mitgespeichert wird.
Die Klasseninformation des deserialisierten Objekts und der Bytecode des Empfängerobjekts müssen kompatibel sein. Dieses Problem wird im nächsten Kapitel näher behandelt.
Weitere Aspekte der Serialisierung:
4.1) Versionierung
Applikationen, in denen Code und Daten getrennt gehalten werden, haben grundsätzlich mit dem Problem der Inkonsistenz beider Bestandteile zu kämpfen. Dieses Problem tritt immer dann auf, wen Code und Daten getrennt voneinander geändert werden. Das kommt bei allen Datenbankanwendungen vor aber auch beim Serialisieren von Objekten.
JAVA versucht dieses Problem mit einem Versionierungsmechanismus zu lösen. Dieser sieht vor, dass das Interface Serializable eine long Konstante serialVersionUID enthält, in der eine Versionskennung zur Klasse gespeichert wird. Sie wird beim Aufruf von writeObject automatisch berechnet und stellt einen Hashcode über die wichtigsten Eigenschaften der Klasse dar. So gehen beispielsweise Name und Signatur der Klasse, implementierte Interfaces sowie Methoden und Konstruktoren in die Berechnung ein. Selbst triviale Anderungen wie das Umbenennen oder Hinzufügen einer öffentlichen Methode verändern sie. Beim Java Paket ist das Programm Serial Version Inspector enthalten, mit welchem die Versionsnummer bestimmt werden kann. Es wird mit dem Namen der Klasse in der Kommandozeile gestartet und liefert die Versionsnummer zurück. Wird es mit dem Parameter -show aufgerufen, wird es mit einer einfachen grafischen Oberfläche gestartet.
Diese Versionsnummer wird beim Serialisieren mit in den Ausgabestream geschrieben und beim Deserialisieren wird diese Nummer mit der des aktuell kompilierten class-Files verglichen. Falls diese nicht übereinstimmen wird eine InvalidClassException ausgelöst und das Deserialisieren wird abgebrochen.
Diese Art der Deserialisierung ist sehr sicher. Das bringt allerdings nicht nur Vorteile mit sich. Wenn zum Beispiel in eine Klasse eine Methode hinzugefügt wird können bereits deserialisierte Objekte nicht mehr verwendet werden, obwohl das für die Wiederherstellung des Objekts nicht von Bedeutung ist.
Dieses Problem kann man aber umgehen indem man die Konstante static final long serialVersionUID definiert und mit einem Wert belegt. Dann wird die Nummer nicht mehr neu berechnet und man kann Anderungen an der Klasse vornehmen ohne, dass das Deserialisieren mit einer Ausnahme abbricht. Jetzt muss natürlich der Entwickler darauf achten dass keine inkompatiblen Anderungen durchgeführt werden. Dazu gibt es einige Regeln die beachtet werden sollten:
Methoden dürfen entfernt oder hinzugefügt werden
Membervariablen dürfen entfernt werden
Membervariablen die hinzugefügt wurden sind nach dem Deserialisieren nicht initialisiert
Problematisch ist es meist, Membervariablen umzubenennen, sie auf transient oder static (oder umgekehrt) zu ändern, die Klasse von Serializable auf Externalizable (oder umgekehrt) zu ändern oder den Klassennamen zu ändern.
4.2) Nicht serialisierte Membervariablen:
Das sind zwei Typen von Membervariablen:
static: Sie gehören nicht zum Objekt sondern zur Klasse des Objekts
transient: Dieses Schlüsselwort wird verwendet, wenn es in einer Klasse Variablen gibt die nicht serialisiert werden sollen.
Beispiele für transiente Attribute:
Werte die sich während des Programmablaufs dynamisch ergeben.
Werte die nur für die temporäre Kommunikation zwischen zwei oder mehreren Methoden von Bedeutung sind.
Daten die nur im Kontext der laufenden Anwendung Sinn machen: Filehandles, GUI-Ressourcen, Sockets,
Diese Attribute werden nach dem Deserialisieren mit dem typenspezifischen Standardwert belegt.
4.3) Objektreferenzen:
Wenn ein Objekt Membervariablen besitzt die ebenfalls Objekte sind, müssen diese Objekte ebenfalls korrekt serialisiert und deserialisiert werden. Das bedeutet dass die Objektreferenzen richtig mitgespeichert werden müssen. Besonders problematisch ist es wenn mehrere Referenzen auf ein Objekt vorhanden sind. Dann darf das Objekt auch nach dem Deserialisieren nur einmal vorhanden sein.
Der ObjectOutputStream enthält aus diesem Grund eine Hashtabelle, in der alle bereits serialisierten Objekte verzeichnet werden. Bei einem Aufruf von writeObject wird zunächst in dieser Tabelle nachgesehen und falls das Objekt bereits serialisiert wurde wird nur ein Verweis auf dieses Objekt gespeichert. Beim Deserialisieren eines Verweises wird dieser durch einen Verweis auf das bereits deserialisierte Objekt ersetzt. Somit werden Objekte nur einmal gespeichert, die Objektreferenzen werden gesichert und Endlosschleifen durch zyklische Verweise werden auch verhindert.
4.4) Externalizable:
Wird anstatt des Interfaces Serializable das Interface Externalizable verwendet wird nur die VersionsID in den Ausgabestream geschrieben. Für das sichern der Daten der Klasse ist diese selber verantwortlich. Es werden hierfür die beiden Methoden writeExternal und readExternal des Externalizable Interfaces implementiert. Beim Serialisieren wird dann writeExternal und beim Deserialisieren wird readExternal aufgerufen. Und der Entwickler kann selber das Format und den Inhalt der gespeicherten Daten bestimmen.
Quellen:
JAVA API SPECIFICATION
Haupt | Fügen Sie Referat | Kontakt | Impressum | Nutzungsbedingungen