CodeGym /Kurse /JAVA 25 SELF /Objektidentität bei der binären Serialisierung

Objektidentität bei der binären Serialisierung

JAVA 25 SELF
Level 44 , Lektion 3
Verfügbar

1. Einleitung

In Java (und im OOP allgemein) hat jedes Objekt eine Identität – das ist nicht einfach die Menge seiner Feldwerte, sondern vielmehr „was dieses Objekt im Speicher ist“. Zwei Objekte können „gleichwertig“ sein (zum Beispiel liefert a.equals(b) true), aber dennoch unterschiedliche Identität besitzen (a != b). Identität bedeutet, dass zwei Variablen auf dasselbe Objekt im Speicher verweisen (a == b).

Warum ist das für die Serialisierung wichtig?

Wenn Sie einen Objektgraphen serialisieren (speichern) – zum Beispiel einen Baum, eine Liste oder einfach zwei Objekte, die auf dasselbe eingebettete Objekt verweisen –, ist es wichtig, nicht nur die Feldwerte, sondern auch die Struktur der Referenzen zwischen den Objekten zu erhalten.

Wenn Ihr Graph geteilte Referenzen (mehrere Objekte verweisen auf dasselbe eingebettete Objekt) oder sogar zyklische Referenzen (A → B → A) enthält, müssen diese Verknüpfungen nach der Deserialisierung genauso bestehen bleiben.

Beispiel: Sie haben zwei Objekte A und B, beide verweisen auf dasselbe Objekt C. Nach Serialisierung und Deserialisierung muss gelten: a.c == b.c (dasselbe Objekt im Speicher). Wenn die Serialisierung einfach nur „Objekte kopiert“, entstehen nach der Wiederherstellung zwei verschiedene C‑Objekte – und das ist ein anderer Graph.

2. Wie ObjectOutputStream das Identitätsproblem löst

In Java wird für die binäre Serialisierung ein Klassenpaar verwendet: ObjectOutputStream (zum Schreiben) und ObjectInputStream (zum Lesen).

ObjectOutputStream ist nicht einfach nur „ein Objekt in eine Datei schreiben“. Er ist intelligent:

  • Er verfolgt alle Objekte, die innerhalb eines Schreibdatenstroms (einer Sitzung) bereits serialisiert wurden.
  • Trifft er auf eine Referenz auf ein bereits serialisiertes Objekt, schreibt er es nicht erneut, sondern einen speziellen „Wiederverweis“ (reference handle).
  • Beim Deserialisieren stellt ObjectInputStream die Struktur so wieder her, dass die Wiederverweise auf dasselbe Objekt im Speicher zeigen.

Das funktioniert sogar bei zyklischen Referenzen! Das heißt, wenn Sie A → B → A haben, geraten Serialisierung und Deserialisierung nicht in eine Endlosschleife und „stürzen“ nicht ab: Die Referenzen werden korrekt wiederhergestellt.

Wie ist das umgesetzt?

Intern verwendet ObjectOutputStream eine spezielle Tabelle (Identity Map), in der bereits serialisierte Objekte gespeichert werden. Bei einem neuen Objekt wird es in die Tabelle aufgenommen und vollständig serialisiert. Bei einem Objekt aus der Tabelle wird in den Stream nur ein „Wiederverweis“ (Handle) geschrieben.

3. Demonstration am Beispiel

Beispiel 1: Graph mit zyklischen Referenzen (A → B, B → A)

import java.io.*;

class Node implements Serializable {
    String name;
    Node next;

    Node(String name) {
        this.name = name;
    }
}

public class CyclicSerializationDemo {
    public static void main(String[] args) throws Exception {
        Node a = new Node("A");
        Node b = new Node("B");
        a.next = b;
        b.next = a; // Zyklus!

        // Serialisierung
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic.dat"))) {
            out.writeObject(a);
        }

        // Deserialisierung
        Node a2;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic.dat"))) {
            a2 = (Node) in.readObject();
        }

        System.out.println("a2.name = " + a2.name); // A
        System.out.println("a2.next.name = " + a2.next.name); // B
        System.out.println("a2.next.next == a2: " + (a2.next.next == a2)); // true!
    }
}

Ausgabe:

a2.name = A
a2.next.name = B
a2.next.next == a2: true

Kommentar:
Der Zyklus wurde beibehalten! Nach der Deserialisierung verweist a2.next.next wieder auf a2.

Beispiel 2: Gemeinsame Referenzen (A → C, B → C)

import java.io.*;

class Wrapper implements Serializable {
    String name;
    Object ref;

    Wrapper(String name) { this.name = name; }
}

public class SharedReferenceDemo {
    public static void main(String[] args) throws Exception {
        Wrapper a = new Wrapper("A");
        Wrapper b = new Wrapper("B");
        Wrapper c = new Wrapper("C");

        a.ref = c;
        b.ref = c;

        // Serialisierung
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("shared.dat"))) {
            out.writeObject(new Wrapper[] {a, b});
        }

        // Deserialisierung
        Wrapper[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("shared.dat"))) {
            arr = (Wrapper[]) in.readObject();
        }

        Wrapper a2 = arr[0];
        Wrapper b2 = arr[1];

        System.out.println("a2.ref == b2.ref: " + (a2.ref == b2.ref)); // true!
        System.out.println("a2.ref.name = " + ((Wrapper)a2.ref).name); // C
    }
}

Ausgabe:

a2.ref == b2.ref: true
a2.ref.name = C

Kommentar:
Nach der Deserialisierung verweisen beide Referenzen wieder auf dasselbe Objekt.

Bezug zu writeReplace und readResolve

In Java kann man mit speziellen Methoden in den (De-)Serialisierungsprozess „eingreifen“:

writeReplace() – wird vor der Serialisierung eines Objekts aufgerufen. Man kann ein anderes Objekt zurückgeben, das anstelle des ursprünglichen serialisiert wird.
readResolve() – wird nach der Deserialisierung eines Objekts aufgerufen. Man kann ein anderes Objekt zurückgeben, das anstelle des soeben erzeugten verwendet wird.

Auswirkung auf die Identität:

  • Wenn writeReplace() oder readResolve() verwendet wird, landet genau das von diesen Methoden zurückgegebene Objekt in der Referenztabelle.
  • Das kann die Identität verändern: Wenn readResolve() ein neues/anderes Objekt zurückgibt, können Wiederverweise auf unterschiedliche Instanzen zeigen.
  • Vorsicht: Sie können die Identität sowohl „zerstören“ als auch gezielt sicherstellen (klassisches Beispiel – Singleton via readResolve(), wobei nach der Deserialisierung alle Verweise auf ein und dieselbe Singleton-Instanz zeigen).

5. Praxis: Code, der den Erhalt der Identität zeigt

import java.io.*;

class Shared implements Serializable {
    String value;
    Shared(String value) { this.value = value; }
}

class Holder implements Serializable {
    String name;
    Shared shared;
    Holder(String name, Shared shared) {
        this.name = name;
        this.shared = shared;
    }
}

public class IdentityDemo {
    public static void main(String[] args) throws Exception {
        Shared c = new Shared("C");
        Holder a = new Holder("A", c);
        Holder b = new Holder("B", c);

        // Serialisierung
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("identity.dat"))) {
            out.writeObject(new Holder[] {a, b});
        }

        // Deserialisierung
        Holder[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("identity.dat"))) {
            arr = (Holder[]) in.readObject();
        }

        Holder a2 = arr[0];
        Holder b2 = arr[1];

        System.out.println("a2.shared == b2.shared: " + (a2.shared == b2.shared)); // true!
        System.out.println("a2.shared.value = " + a2.shared.value); // C
    }
}

Ausgabe:

a2.shared == b2.shared: true
a2.shared.value = C

Kommentar:
Die Identität des Objekts c wurde beibehalten: Nach der Deserialisierung verweisen beide Referenzen auf dieselbe Instanz.

6. Fazit und typische Fehler

Fehler Nr. 1: Separate Serialisierung von Objekten mit geteilten Referenzen. Wenn Sie jedes Objekt mit einer eigenen Instanz von ObjectOutputStream schreiben, bleiben die Referenzen zwischen ihnen nicht erhalten – die deserialisierten Instanzen werden unterschiedlich sein, selbst wenn es im Original dieselbe Referenz war.

Fehler Nr. 2: Falscher Einsatz von writeReplace() und readResolve(). Diese Methoden können das Objekt während der (De‑)Serialisierung ersetzen, was seine Identität verändert. Ohne Verständnis der Mechanik können am Ende unerwartete Instanzen entstehen.

Fehler Nr. 3: Unerwartete Effekte durch gemeinsame veränderliche Referenzen. Wenn mehrere Objekte auf ein veränderliches eingebettetes Objekt verweisen (zum Beispiel eine Liste), bleibt das nach der Deserialisierung genauso. Eine Änderung an einer Stelle wirkt sich auf alle anderen aus.

Fehler Nr. 4: Erwartung „neuer“ Objekte nach der Serialisierung. Die Serialisierung erzeugt keine „frische“ Struktur – sie stellt die ursprünglichen Referenzen wie im Original wieder her. Das kann bei Caches oder Vorlageobjekten überraschend sein.

1
Aufgabe
JAVA 25 SELF, Level 44, Lektion 3
Gesperrt
Gemeinsame Arbeit an einem geteilten Dokument: Erhaltung geteilter Objekte 📝
Gemeinsame Arbeit an einem geteilten Dokument: Erhaltung geteilter Objekte 📝
1
Aufgabe
JAVA 25 SELF, Level 44, Lektion 3
Gesperrt
Inventarverwaltung: Identität von Objekten bei getrennter Speicherung 📦
Inventarverwaltung: Identität von Objekten bei getrennter Speicherung 📦
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION