CodeGym /Kurse /JAVA 25 SELF /Problem zyklischer Referenzen: Erkennung und Umgehung

Problem zyklischer Referenzen: Erkennung und Umgehung

JAVA 25 SELF
Level 44 , Lektion 2
Verfügbar

1. Was sind zyklische Referenzen?

Eine zyklische Referenz ist eine Situation, in der ein Objekt (oder eine Collection) direkt oder indirekt auf sich selbst verweist. In Collections kommt das häufiger vor, als man denkt, insbesondere wenn Sie komplexe Datenstrukturen aufbauen oder mit Graphen arbeiten.

Beispiele aus der Praxis

  • Zwei Objekte verweisen gegenseitig aufeinander:
    Zum Beispiel gibt es eine Klasse User, die eine Referenz auf Profile hält, und Profile verweist wiederum zurück auf User.
  • Eine Collection enthält sich selbst:
    Das einfachste und „witzige“ Beispiel:
List<Object> list = new ArrayList<>();
list.add(list); // Hoppla! list enthält sich selbst
  • Objektgraf:
    Zusammenhängende Objekte, z. B. Baumknoten, bei denen jeder eine Referenz auf den Elternknoten und auf die Kinder haben kann.

Visualisierung

graph LR
A[User] -- profile --> B[Profile]
B -- user --> A

Oder für eine Collection:

graph TD
L[List] -- add(self) --> L

Warum kann das problematisch sein?

Wenn der Serialisierer keine Zyklen erkennen kann, kann er „ins Unendliche“ laufen, indem er verschachtelte Objekte immer wieder erneut serialisiert, bis der Stack überläuft (StackOverflowError). Gute Nachricht: Die Standardserialisierung von Java kennt solche Tricks und kann damit umgehen!

2. Wie funktioniert die Standardserialisierung von Java mit Zyklen?

Wenn Sie ein Objekt über ObjectOutputStream serialisieren, verfolgt Java automatisch, welche Objekte in diesem Stream bereits serialisiert wurden. Trifft der Serialisierer ein Objekt erneut, serialisiert er es nicht noch einmal, sondern schreibt einen speziellen Verweis auf das bereits serialisierte Objekt. Dadurch lassen sich selbst sehr komplexe Strukturen mit Zyklen korrekt serialisieren.

Beispiel: Collection, die sich selbst enthält

Versuchen wir, eine Collection zu serialisieren, die sich selbst enthält. Das ist kein Scherz – solcher Code kompiliert und funktioniert sogar:

import java.io.*;
import java.util.*;

public class CyclicListDemo {
    public static void main(String[] args) throws Exception {
        List<Object> list = new ArrayList<>();
        list.add("Hello, cyclic world!");
        list.add(list); // Fügen wir uns selbst hinzu

        // Serialisierung
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic_list.ser"))) {
            out.writeObject(list);
        }

        // Deserialisierung
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic_list.ser"))) {
            List<?> deserialized = (List<?>) in.readObject();

            System.out.println(deserialized.get(0)); // "Hello, cyclic world!"
            System.out.println(deserialized.get(1) == deserialized); // true!
        }
    }
}

Ergebnis:
– Das erste Element ist ein normaler String.
– Das zweite Element ist ... die Collection selbst! Der Test deserialized.get(1) == deserialized ergibt true.
Java ist nicht in eine Endlosschleife geraten und nicht abgestürzt, sondern hat die Referenzstruktur korrekt wiederhergestellt.

Wie funktioniert das intern?

ObjectOutputStream führt ein internes „Register“ der serialisierten Objekte. Wurde ein Objekt bereits serialisiert, wird in den Stream ein spezieller Verweis (Handle) geschrieben, nicht dessen Inhalt. Bei der Deserialisierung stellt ObjectInputStream genau diese Verbindungen wieder her.

3. Probleme und Einschränkungen

  • Aus Versehen einen riesigen Graphen serialisiert.
    Wenn Ihre Datenstruktur sehr groß ist und viele Querverweise enthält, kann die Serialisierung lange dauern und eine riesige Datei erzeugen.
  • Änderung der Klassenstruktur.
    Wenn Sie ein Objekt serialisiert haben und danach dessen Klasse ändern (z. B. ein Feld hinzufügen oder entfernen), kann bei der Deserialisierung eine InvalidClassException auftreten. Besonders heikel ist das, wenn Felder betroffen sind, die am Zyklus beteiligt sind.
  • Probleme bei benutzerdefinierter Serialisierung.
    Wenn Sie die Methoden writeObject und readObject manuell implementieren, müssen Sie Zyklen selbst korrekt behandeln. Wenn Sie die Standardmethoden nicht aufrufen (defaultWriteObject/defaultReadObject), kann der Serialisierer Zyklen nicht nachverfolgen.
  • Serialisierung in andere Formate (z. B. JSON).
    Die Standardserialisierung von Java (ObjectOutputStream) kommt mit Zyklen zurecht, aber wenn Sie Objekte in JSON serialisieren (z. B. mit Jackson oder Gson), können Zyklen zu StackOverflowError oder Ausnahmen führen. Solche Bibliotheken können standardmäßig nicht mit Zyklen umgehen – es sind explizite Einstellungen nötig.

4. Umgang mit zyklischen Referenzen

In der Java-Standardserialisierung

Alles funktioniert „out of the box“! Sie müssen nichts Besonderes tun – Java erkennt Zyklen selbst und bewahrt die Referenzstruktur.

Manuell: Serialisierung in andere Formate

  • IDs statt Referenzen verwenden.
    Anstatt Referenzen auf andere Objekte zu speichern, speichern Sie deren eindeutige IDs. Nach der Deserialisierung stellen Sie die Verbindungen anhand dieser IDs wieder her.
  • Spezielle Annotationen oder Einstellungen.
    In Jackson können Sie die Annotation @JsonIdentityInfo oder die Kombination @JsonBackReference/@JsonManagedReference verwenden, um die Serialisierung von Zyklen zu steuern.
  • Zyklen vor der Serialisierung entfernen.
    Setzen Sie Felder, die einen Zyklus erzeugen, vorübergehend auf null, schließen Sie sie mittels transient oder per Annotation aus.

Beispiel: Serialisierung eines Graphen mit Zyklen

Betrachten wir ein Beispiel mit einer komplexeren Struktur – einem Graphen von Benutzern, bei dem jeder Benutzer Freund eines anderen Benutzers sein kann.

import java.io.*;
import java.util.*;

class User implements Serializable {
    String name;
    List<User> friends = new ArrayList<>();

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

    public String toString() {
        return name + " (" + friends.size() + " friends)";
    }
}

public class CyclicGraphDemo {
    public static void main(String[] args) throws Exception {
        User alice = new User("Alice");
        User bob = new User("Bob");
        User charlie = new User("Charlie");

        // Freundschaften mit Zyklen herstellen
        alice.friends.add(bob);
        bob.friends.add(charlie);
        charlie.friends.add(alice); // Zyklus!

        // Serialisierung
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
            out.writeObject(alice);
        }

        // Deserialisierung
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("users.ser"))) {
            User restoredAlice = (User) in.readObject();
            System.out.println(restoredAlice);
            System.out.println(restoredAlice.friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0).friends.get(0) == restoredAlice); // true!
        }
    }
}

Ergebnis:
– Die Struktur mit Zyklus wird wiederhergestellt: Nach drei Übergängen über Freunde landet man wieder bei Alice.
– Java hat sich nicht verwirrt und ist nicht in eine Endlosschleife geraten.

5. Typische Fehler beim Umgang mit zyklischen Referenzen

Fehler Nr. 1: Serialisierung nach JSON ohne Unterstützung für Zyklen. Wenn Sie ein Objekt mit Zyklen über Jackson oder Gson ohne Konfiguration serialisieren, erhalten Sie höchstwahrscheinlich einen StackOverflowError. Wenn Sie z. B. eine Klasse Node haben, in der jeder Knoten auf den Elternknoten und auf die Kinder verweist, führt die Serialisierung eines solchen Baums nach JSON zu unendlicher Verschachtelung.

Fehler Nr. 2: Verletzung der Klassenstruktur. Wenn nach der Serialisierung die Klassenstruktur geändert wird (z. B. ein Feld hinzugefügt wird), kann bei der Deserialisierung der alten Datei ein Inkompatibilitätsfehler auftreten. Das ist besonders kritisch bei komplexen Graphen mit Zyklen.

Fehler Nr. 3: Eigenbau-Serialisierung ohne Berücksichtigung von Zyklen. Wenn Sie writeObject/readObject manuell implementieren und defaultWriteObject nicht aufrufen, kann Java Zyklen nicht nachverfolgen, und die Serialisierung läuft entweder in eine Schleife oder die Referenzstruktur bricht bei der Deserialisierung.

Fehler Nr. 4: Eine Collection landet versehentlich in sich selbst. Unerfahrene Entwickler fügen manchmal versehentlich eine Collection in sich selbst ein (z. B. beim Kopieren von Elementen), ohne zu erkennen, dass sie einen Zyklus erzeugt haben. Die Serialisierung funktioniert dann zwar, aber die Programmlogik kann seltsam und unvorhersehbar werden.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION