Serialisierung von Objekten und Zuständen
Objekte lassen sich mit der Methode void writeObject(Object)
in ObjectOutputStream
einfach in einen Datenstrom schreiben. Dies kann beispielsweise eine Datei o.ä. sein.
Mit Hilfe von Object readObject()
in ObjectInputStream
lassen sich Objekte aus dem
Datenstrom auch wieder herstellen. Dies nennt man Serialisierung und De-Serialisierung.
Um Objekte einer Klasse serialisieren zu können, muss diese das leere Interface
Serializable
implementieren ("Marker-Interface"). Damit wird quasi die Unterstützung
in Object*Stream
freigeschaltet.
Wenn ein Objekt serialisiert wird, werden alle Attribute in den Datenstrom geschrieben, d.h. die Typen der Attribute müssen ihrerseits serialisierbar sein. Dies gilt für alle primitiven Typen und die meisten eingebauten Typen. Die Serialisierung erfolgt ggf. rekursiv, Zirkelreferenzen werden erkannt und aufgebrochen.
static
und transient
Attribute werden nicht serialisiert.
Beim De-Serialisieren wird das neue Objekt von der Laufzeitumgebung aus dem Datenstrom rekonstruiert. Dies geschieht direkt, es wird kein Konstruktor involviert.
Beim Serialisieren wird für die Klasse des zu schreibenden Objekts eine serialVersionUID
berechnet und mit gespeichert. Beim Einlesen wird dann geprüft, ob die serialisierten
Daten zur aktuellen Version der Klasse passen. Da dies relativ empfindlich gegenüber
Änderungen an einer Klasse ist, wird empfohlen, selbst eine serialVersionUID
pro
Klasse zu definieren.
- (K2) Was ist ein Marker-Interface und warum ist dies eine der großen Design-Sünden in Java?
- (K2) Erklären Sie den Prozess der Serialisierung und De-Serialisierung. Worauf müssen Sie achten?
- (K3) Serialisierung von Objekten und Programmzuständen
- (K3) Serialisierung eigener Klassen und Typen
Motivation: Persistierung von Objekten und Spielzuständen
public class Studi {
private final int credits = 42;
private String name = "Hilde";
...
}
Wie kann ich Objekte speichern und wieder laden?
Ich möchte ein Spiel (einen Lauf) im Dungeon abspeichern, um es später fortsetzen zu können. Wie kann ich den aktuellen Zustand (also Level, Monster, Held, Inventar, XP/Health/...) so speichern, dass ich später das Spiel nach einem Neustart einfach fortsetzen kann?
Serialisierung von Objekten
-
Klassen müssen Marker-Interface
Serializable
implementieren"Marker-Interface": Interface ohne Methoden. Ändert das Verhalten des Compilers, wenn eine Klasse dieses Interface implementiert: Weitere Funktionen werden "freigeschaltet", beispielsweise die Fähigkeit, Klone zu erstellen (
Cloneable
) oder beiSerializable
Objekte serialisierbar zu machen.Das ist in meinen Augen eine "Design-Sünde" in Java (neben der Einführung von
null
): Normalerweise definieren Interfaces eine Schnittstelle, die eine das Interface implementierende Klasse dann erfüllen muss. Damit agiert das Interface wie ein Typ. Hier ist das Interface aber leer, es wird also keine Schnittstelle definiert. Aber es werden damit stattdessen Tooling-Optionen aktiviert, was Interfaces vom Konzept her eigentlich nicht machen sollten/dürften - dazu gibt es Annotationen! -
Schreiben von Objekten (samt Zustand) in Streams
ObjectOutputStream: void writeObject(Object)
Die Serialisierung erfolgt dabei für alle Attribute (außer
static
undtransient
, s.u.) rekursiv.Dabei werden auch Zirkelreferenzen automatisch aufgelöst/unterbrochen.
-
Lesen und "Wiedererwecken" der Objekte aus Streams
ObjectInputStream: Object readObject()
Dabei erfolgt KEIN Konstruktor-Aufruf!
Einfaches Beispiel
public class Studi implements Serializable {
private final int credits = 42;
private String name = "Hilde";
public static void writeObject(Studi studi, String filename) {
try (FileOutputStream fos = new FileOutputStream(filename);
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(studi); oos.close();
} catch (IOException ex) {}
}
public static Studi readObject(String filename) {
Studi studi = null;
try (FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis)) {
studi = (Studi) ois.readObject(); ois.close();
} catch (IOException | ClassNotFoundException ex) {}
return studi;
}
}
Bedingungen für Objekt-Serialisierung
- Klassen implementieren Marker-Interface
Serializable
- Alle Attribute müssen ebenfalls serialisierbar sein (oder Deklaration "
transient
") - Alle primitiven Typen sind per Default serialisierbar
- Es wird automatisch rekursiv serialisiert, aber jedes Objekt nur einmal (bei Mehrfachreferenzierung)
- Serialisierbarkeit vererbt sich
Ausnahmen
- Als
static
deklarierte Attribute werden nicht serialisiert - Als
transient
deklarierte Attribute werden nicht serialisiert - Nicht serialisierbare Attribut-Typen führen zu
NotSerializableException
Version-UID
static final long serialVersionUID = 42L;
- Dient zum Vergleich der serialisierten Version und der aktuellen Klasse
- Über IDE generieren oder manuell vergeben
- Wenn das Attribut fehlt, wird eine Art Checksumme von der Runtime-Umgebung berechnet (basierend auf diversen Eigenschaften der Klasse)
Dieser Wert wird beim Einlesen verglichen: Das Objekt wird nur dann wieder de-serialisiert,
wenn die serialVersionUID
mit der einzulesenden Klasse übereinstimmt!
Bei automatischer Berechnung der serialVersionUID
durch die JVM kann jede kleine Änderung an
der Klasse (beispielsweise Refactoring: Änderung der Methodennamen) eine neue serialVersionUID
zur Folge haben. Das würde bedeuten, dass bereits serialisierte Objekte nicht mehr eingelesen
werden können, auch wenn sich nur Methoden o.ä. verändert haben und die Attribute noch so vorhanden
sind. Deshalb bietet es sich an, hier selbst eine serialVersionUID
zu definieren - dann muss
man aber auch selbst darauf achten, diese zu verändern, wenn sich wesentliche strukturelle
Änderungen an der Klasse ergeben!
Bemerkungen
Es existieren diverse weitere Fallstricke und Probleme, siehe [Bloch2018] Kapitel 11 "Serialization".
Man kann in den ObjectOutputStream
nicht nur ein Objekt schreiben, sondern mehrere Objekte und
Variablen schreiben lassen. In dieser Reihenfolge muss man diese dann aber auch wieder aus dem
Stream herauslesen (vgl. Object Streams).
Man kann die zu serialisierenden Attribute mit der Annotation @Serial
markieren. Dies ist in
der Wirkung ähnlich zu @Override
: Der Compiler prüft dann, ob die markierten Attribute wirklich
serialisierbar sind und würde sonst zur Compile-Zeit einen Fehler werfen.
Weitere Links:
- Tutorials:
- API:
Wrap-Up
-
Markerinterface
Serializable
schaltet Serialisierbarkeit frei -
Objekte schreiben:
ObjectOutputStream
:void writeObject(Object)
-
Objekte lesen:
ObjectInputStream
:Object readObject()
-
Wichtigste Eigenschaften:
- Attribute müssen serialisierbar sein
transient
undstatic
Attribute werden nicht serialisiert- De-Serialisierung: KEIN Konstruktor-Aufruf!
- Serialisierbarkeit vererbt sich
- Objekt-Referenz-Graph wird automatisch beachtet
Implementieren Sie die beiden Klassen entsprechend dem UML-Diagram:
Objekte vom Typ Person
sowie Address
sollen serialisierbar sein (vgl. Vorlesung).
Dabei soll das Passwort nicht serialisiert bzw. gespeichert werden, alle anderen
Eigenschaften von Person
sollen serialisierbar sein.
Hinweis: Verwenden Sie zur Umsetzung java.io.Serializable.
Erstellen Sie in Ihrem main()
einige Instanzen von Person und speichern Sie diese in
serialisierter Form und laden (deserialisieren) Sie diese anschließend in neue Variablen.
Betrachten Sie die ursprünglichen und die wieder deserialisierten Objekte mit Hilfe des Debuggers. Alternativ können Sie die Objekte auch in übersichtlicher Form über den Logger ausgeben.
- [Bloch2018] Effective Java
Bloch, J., Addison-Wesley, 2018. ISBN 978-0-13-468599-1. - [Java-SE-Tutorial] The Java Tutorials
Oracle Corporation, 2022.
Essential Java Classes \> Basic I/O \> Object Streams