09.08 Objekte speichern und laden (serialisieren/deserialisieren)
In Java haben Sie nicht nur die Möglichkeit einfache Datenstrukturen wie Bytes und Zeichenketten zu schreiben und lesen, Sie können auch komplette Objekte durch Streams schicken. Hierzu verwendet man die Klassen java.io.ObjectOutputStream
und java.io.ObjectInputStream
, welche ich Ihnen in diesem Kapitel vorstellen werde.
Voraussetzungen
Beim Speichern von kompletten Objekten besteht jedoch eine Voraussetzung: Das zu speichernde Objekt und alle Attribute müssen das Interface java.io.Serializable
implementieren. Dadurch wird das Objekt als serialisierbar (in diesem Fall schreibbar) gekennzeichnet. Da es nicht sinnvoll ist, jedes beliebige Objekt zu speichern (denken Sie bspw. an Netzwerkverbindungen oder Objekte, die nur für den aktuellen Programmlauf gültig sind), wird dieser Schritt sinnvoll und notwendig.
Ein Objekt schreiben
Zuerst benötigen Sie natürlich ein serialisierbares Objekt. Hierzu verwenden wir eine einfache Klasse mit drei Attributen, die das Interface Serializable
implementiert.
package de.jbb.io; import java.io.Serializable; public class SerializableObject implements Serializable { private String aStringValue; private int aIntValue; private boolean aBooValue; public SerializableObject() {} public SerializableObject(String stringValue, int intValue, boolean booValue) { this.aStringValue = stringValue; this.aIntValue = intValue; this.aBooValue = booValue; } public String getAStringValue() { return this.aStringValue; } public void setAStringValue(String stringValue) { this.aStringValue = stringValue; } public int getAIntValue() { return this.aIntValue; } public void setAIntValue(int intValue) { this.aIntValue = intValue; } public boolean isABooValue() { return this.aBooValue; } public void setABooValue(boolean booValue) { this.aBooValue = booValue; } }
Einem ObjectOutputStream
wird nun ein anderer OutputStream
im Konstruktor übergeben. Als Beispiel verwenden wir den bereits bekannten FileOutputStream
um unser Objekt auf die Festplatte zu schreiben.
ObjectOutputStream oos = null; FileOutputStream fos = null; try { fos = new FileOutputStream("C:/test.ser"); oos = new ObjectOutputStream(fos); } catch (IOException e) { e.printStackTrace(); } finally { if (oos != null) try { oos.close(); } catch (IOException e) {} if (fos != null) try { fos.close(); } catch (IOException e) {} }
Ein serialisierbares Objekt kann nun mittels writeObject(Object obj)
der Klasse ObjectOutputStream
geschrieben werden.
SerializableObject so = new SerializableObject("String", 1, true); oos.writeObject(so);
Über einen java.io.ObjectInputStream
kann das geschriebene Objekt wieder in Ihr Programm geladen werden.
ObjectInputStream ois = null; FileInputStream fis = null; try { fis = new FileInputStream("C:/test.ser"); ois = new ObjectInputStream(fis); Object obj = ois.readObject(); if (obj instanceof SerializableObject) { SerializableObject so = (SerializableObject)obj; System.out.println(so.getAStringValue()); // String System.out.println(so.getAIntValue()); // 1 System.out.println(so.isABooValue()); // true } } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { if (ois != null) try { ois.close(); } catch (IOException e) {} if (fis != null) try { fis.close(); } catch (IOException e) {} }
Wenn Sie ein Attribut übrigens als transient
markieren (siehe Kapitel 04.07 Weitere Modifizierer), wird dieses beim Serialisieren und Deserialisieren ignoriert. Es behält also in jedem Fall seinen Standardwert. Dies ist für serialisierbare Objekte nützlich, die selbst Objekte anderer Klassen als Attribute referenzieren, welche nicht serialisierbar sind. Alle Attribute eines Objekts müssen nämlich serialisierbar (oder eben transient
) sein, damit das Objekt gespeichert werden kann.
private transient NotSerializableObject nso;
Versionskontrolle
Vielleicht ist Ihnen beim Compilieren bzw. in Ihrer IDE bereits eine Warnung aufgefallen, die besagt, dass das „öffentliche, statische, finale Attribut serialVersionUID
vom Typ long
“ fehlen würde. Jede serialisierbare Klasse sollte eine solche ID als Attribut beinhalten. Dies könnte bspw. wie folgt aussehen:
public class SerializableObject implements Serializable { private static final long serialVersionUID = -6184783110522368225L; // ... }
Diese ID ist die Version der Klasse. Was diese Version für einen Sinn hat, lässt sich an einem einfachen Beispiel nachvollziehen. Speichern Sie hierzu das Objekt der Klasse ein weiteres Mal auf die Festplatte – dieses Mal aber mit einer serialVersionUID
(wie eben gezeigt) in der Klasse. Anschließend benennen Sie die Attribute und Methoden unserer Klasse wie folgt um:
public class SerializableObject implements Serializable { private static final long serialVersionUID = -6184783110522368225L; private String stringValue; private int intValue; private boolean booValue; public SerializableObject() {} public SerializableObject(String stringValue, int intValue, boolean booValue) { this.stringValue = stringValue; this.intValue = intValue; this.booValue = booValue; } public String getStringValue() { return this.stringValue; } public void setStringValue(String stringValue) { this.stringValue = stringValue; } public int getIntValue() { return this.intValue; } public void setIntValue(int intValue) { this.intValue = intValue; } public boolean isBooValue() { return this.booValue; } public void setBooValue(boolean booValue) { this.booValue = booValue; } }
Änderungen an der serialVersionUID
nehmen Sie bitte nicht vor. Nun laden Sie das Objekt erneut in Ihr Programm, jedoch ohne das Objekt zuvor noch einmal auf die Festplatte zu schreiben. Als Ausgabe erhalten Sie nun nicht mehr
String
1
true
sondern
null
0
false
Eben die default-Werte der Attribute. Durch die identische serialVersionUID
der Klasse geht Java davon aus, dass sich nichts an der Struktur geändert hat. Jedoch können die zuvor anders benannten Felder nun nicht mehr unserer umstrukturierten Klasse zugeordnet werden. Sie als Programmierer haben jetzt keine Möglichkeit mehr zu überprüfen, ob in dem gespeicherten Objekt wirklich null, 0
und false
stand, oder ob diese Werte zustande gekommen sind, weil sich etwas am Klassenaufbau geändert hat.
Verändern Sie nun die serialVersionUID
(beliebiger Wert) und versuchen Sie die Klasse erneut zu laden (natürlich zuvor nicht wieder abspeichern). Bspw.:
private static final long serialVersionUID = -123456789L;
Es wird eine Exception geworfen:
java.io.InvalidClassException: de.jbb.io.SerializableObject; local class incompatible: stream classdesc serialVersionUID = -6184783110522368225, local class serialVersionUID = -123456789
at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
at java.io.ObjectInputStream.readClassDesc(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
Auf diesem Weg können Sie abfangen, falls ein Objekt gelesen werden soll, welches noch vor einer Änderung der Klasse geschrieben wurde. Bearbeiten Sie deshalb nach jeder Änderung der Attribute Ihrer Klasse die serialVersionUID
!
Generierte serialVersionUID
Geben Sie keine serialVersionUID
in Ihrer serialisierbaren Klasse an, erzeugt der Compiler eine Solche automatisiert. Dies hat den Vorteil, dass Sie sich nicht um die Generierung und Aktualisierung der serialVersionUID
kümmern müssen. Die Warnung, die der Compiler beim Kompilieren ausgibt, können Sie mit einer Annotation unterdrücken.
@SuppressWarnings("serial") public class SerializableObject implements Serializable {}
Der Nachteil ist jedoch, dass Sie nicht selbst bestimmen können, wann eine Klasse nicht mehr mit einer vorhergehenden Version kompatibel ist. Erzeugen Sie bspw. eine neue, nicht private Methode, erzeugt der Compiler auch eine neue UID, obwohl dies meistens gar nicht nötig wäre.
Schöner Artikel! Ich würde der Vollständigkeit halber aber noch erwähnen wollen, dass es unter Berücksichtigung der Performance durchaus Sinn macht
DataInputStream
undDataOutputStream
zum „serialisieren“ zu verwenden, auch wenn man alle Klassenvariablen händisch in die Streams schreiben bzw. wieder rausholen muss.Gruss, Johannes
Hallo Johannes,
da haben Sie natürlich recht. Es kommt jedoch immer auf das zu schreibende Objekt an. Ich würde nur ungern sehr tief verschachtelte Objekte mit vielen Attributen manuell über einen
DataOutputStream
schreiben. Jedoch sollte man sich immer überlegen, obObjectInputStreams
/ObjectOutputStreams
überhaupt an dieser Stelle notwendig/richtig sind.Zum Thema
DataInputStream
bzw.DataOutputStream
möchte ich an dieser Stelle noch auf das Kapitel 09.05 Beliebige Daten lesen und schreiben verweisen. Dort ist ein kleines Beispiel für diese Streams zu finden.Gruß
Stefan
Hi Stefan, soweit ich weiß hat Sun für serialisierte Objekte die Dateiendung .ser vorgesehen.
Hi LeX,
danke für den Hinweis. Ich habe die Dateiendungen im Kapitel entsprechend angepasst. Da man aber prinzipiell eine beliebige Dateiendung (solange diese noch nicht verwendet wird) wählen kann, verzichte ich auf einen expliziten Hinweis im Kapitel.
Gruß
Stefan
Hi,
vielleicht könnte man der Vollständigkeit halber noch erwähnen, dass mit dem Schlüsselwort transient gekennzeichnete Instanzvariablen beim Serialisieren überprungen werden. Diese müssen folglich dann auch nicht Serializable implementieren.
Ciao
DosCoder
Hi DosCoder,
dieser Hinweis ist im Kapitel 04.07 Weitere Modifizierer zu finden. Da es selbstverständlich sinnvoll wäre, auch in diesem Kapitel darauf zu verweisen, habe ich das Kapitel um einen weiteren Absatz ergänzt.
Gruß
Stefan