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.

6 Replies to “09.08 Objekte speichern und laden (serialisieren/deserialisieren)”

  1. Johannes Schaback

    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 und DataOutputStream zum „serialisieren“ zu verwenden, auch wenn man alle Klassenvariablen händisch in die Streams schreiben bzw. wieder rausholen muss.

    Gruss, Johannes

  2. Stefan Kiesel

    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, ob ObjectInputStreams/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

  3. Stefan Kiesel

    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

  4. DosCoder

    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

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.