CodeGym /Kurse /JAVA 25 SELF /Serialisierungsverhalten anpassen: benutzerdefinierte Met...

Serialisierungsverhalten anpassen: benutzerdefinierte Methoden

JAVA 25 SELF
Level 43, Lektion 3
Verfügbar

1. Die Methoden writeReplace und readResolve: Theorie

Manchmal reichen die Standardmittel der Serialisierung nicht aus. Stellen Sie sich eine Situation vor: Sie haben einen Singleton (eine Klasse, von der es im gesamten Programm nur eine einzige Instanz geben darf), und Sie möchten, dass er nach der Deserialisierung weiterhin die einzige Instanz bleibt (und kein neuer Klon entsteht). Oder Sie möchten nicht das Objekt selbst serialisieren, sondern seine „abgespeckte“ Version (einen Proxy), um Implementierungsdetails zu verbergen oder Platz zu sparen.

In Java gibt es dafür spezielle Methoden: writeReplace und readResolve. Ihre Aufgabe besteht darin, das zu serialisierende bzw. deserialisierte Objekt durch ein anderes zu ersetzen.

Einfache Analogie:
Es ist, als würden Sie einem Freund ein Paket schicken, aber statt Ihrer selbst legen Sie eine Spielzeug-Doppelgängerfigur in den Karton. Und wenn der Freund das Paket auspackt, hält er statt des Spielzeugs plötzlich Sie – das Original – in den Händen! (Im echten Leben funktioniert das nicht, in Java jedoch durchaus.)

writeReplace

Die Methode private Object writeReplace() wird am Objekt vor der Serialisierung aufgerufen. Sie kann ein beliebiges Objekt zurückgeben, das anstelle des ursprünglichen tatsächlich serialisiert wird. Ist sie nicht implementiert, wird das Objekt selbst serialisiert.

Signatur:


private Object writeReplace() throws ObjectStreamException

readResolve

Die Methode private Object readResolve() wird am Objekt nach der Deserialisierung aufgerufen. Sie ermöglicht es, das soeben erzeugte Objekt durch ein anderes zu ersetzen (z. B. einen Singleton oder einen gecachten Instanzwert zurückzugeben).

Signatur:

private Object readResolve() throws ObjectStreamException

Wichtig:
Beide Methoden müssen private sein und Object zurückgeben. Das ist eine Anforderung der Java-Serialisierungsspezifikation. Wenn Sie sie public machen, ignoriert die Serialisierung sie einfach.

2. Anwendung von writeReplace und readResolve in der Praxis

Singleton und readResolve

Ein Singleton ist einfach eine Klasse, von der es im gesamten Programm nur eine einzige Instanz geben darf. Wenn man ein solches Objekt serialisiert und anschließend wiederherstellt, entsteht ohne die Methode readResolve eine neue Instanz, und die „Einzigkeit“ wird verletzt. Mit readResolve kann man genau dieses eine Objekt zurückgeben und so die Idee des Singletons bewahren.

import java.io.*;

public class MySingleton implements Serializable {
    private static final MySingleton INSTANCE = new MySingleton();
    private MySingleton() {}

    public static MySingleton getInstance() {
        return INSTANCE;
    }

    // Gewährleistet, dass nach der Deserialisierung genau INSTANCE zurückgegeben wird
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

Erläuterung:
Ohne readResolve entsteht nach der Deserialisierung ein neues Objekt, das (per ==) nicht dem ursprünglichen Singleton entspricht. Mit readResolve wird immer INSTANCE zurückgegeben.

Prüfen wir das in der Praxis:

MySingleton s1 = MySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.bin"));
out.writeObject(s1);
out.close();

ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.bin"));
MySingleton s2 = (MySingleton) in.readObject();
in.close();

System.out.println(s1 == s2); // true, wenn readResolve vorhanden ist; false — ohne

writeReplace: Serialisierung eines Proxy-Objekts

Manchmal ist ein Objekt zu schwer für die Serialisierung, enthält vertrauliche Daten oder sollte einfach nicht in voller Form nach außen gelangen. In diesem Fall kann man einen Ersatz serialisieren – ein Proxy-Objekt.

Beispiel:
Angenommen, wir haben eine Klasse User mit einem privaten Passwort. Wir möchten nicht, dass das Passwort serialisiert wird.

import java.io.*;

public class User implements Serializable {
    private String username;
    private transient String password; // transient — wird nicht serialisiert

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Statt User serialisieren wir nur UserProxy
    private Object writeReplace() throws ObjectStreamException {
        return new UserProxy(username);
    }

    // Proxy-Klasse — nur für die Serialisierung
    private static class UserProxy implements Serializable {
        private String username;
        public UserProxy(String username) {
            this.username = username;
        }

        private Object readResolve() throws ObjectStreamException {
            // In der Praxis lässt sich das Passwort nicht rekonstruieren — wir geben User mit leerem Passwort zurück
            return new User(username, "");
        }
    }
}

Erläuterung:

  • Bei der Serialisierung wird User in UserProxy umgewandelt (ohne Passwort).
  • Bei der Deserialisierung wird UserProxy wieder in User umgewandelt (das Passwort ist jedoch leer).

3. Anpassung der Serialisierung für unveränderliche Objekte

Unveränderliche (immutable) Objekte verwenden häufig private final-Felder und besitzen keine Setter. Die Standardserialisierung von Java kann diese Einschränkung zwar umgehen, doch oft ist es besser, den Prozess explizit über writeReplace/readResolve zu steuern.

Beispiel: Value Object

import java.io.*;

public final class Money implements Serializable {
    private final int amount;
    private final String currency;

    public Money(int amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    private Object writeReplace() throws ObjectStreamException {
        return new MoneyProxy(amount, currency);
    }

    private static class MoneyProxy implements Serializable {
        private final int amount;
        private final String currency;

        MoneyProxy(int amount, String currency) {
            this.amount = amount;
            this.currency = currency;
        }

        private Object readResolve() throws ObjectStreamException {
            return new Money(amount, currency);
        }
    }
}

Erläuterung:

  • Bei der Serialisierung wird Money zu MoneyProxy (POJO).
  • Bei der Deserialisierung wird MoneyProxy wieder zu Money.

Zusammenspiel mit writeObject/readObject

Die Methoden writeReplace/readResolve arbeiten unabhängig von writeObject/readObject. Wenn beide Mechanismen definiert sind, wird zuerst writeReplace aufgerufen und erst am zurückgegebenen Objekt — writeObject (wenn es Serializable implementiert).

Ablauf:

flowchart LR
    A[Objekt] -- writeReplace --> B[Proxy-Objekt]
    B -- writeObject --> C[Bytestrom]
    C -- readObject --> D[Proxy-Objekt]
    D -- readResolve --> E[Ergebnisobjekt]

4. Praxis: Serialisierung mit Objektersatz

Fügen wir Ihrem Lernprojekt eine benutzerdefinierte Serialisierung hinzu — beispielsweise für die Klasse Person, sodass bei der Serialisierung nur der Name geschrieben wird und das Alter ignoriert wird (nehmen wir an, wir achten auf Privatsphäre).

Schritt 1. Basisklasse

import java.io.*;

public class Person implements Serializable {
    private String name;
    private int age; // soll nicht serialisiert werden

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private Object writeReplace() throws ObjectStreamException {
        return new PersonProxy(name);
    }

    private static class PersonProxy implements Serializable {
        private final String name;

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

        private Object readResolve() throws ObjectStreamException {
            return new Person(name, -1); // -1 — "Alter unbekannt"
        }
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

Schritt 2. Testen

public class TestCustomSerialization {
    public static void main(String[] args) throws Exception {
        Person original = new Person("Alice", 30);

        // Serialisierung
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
        out.writeObject(original);
        out.close();

        // Deserialisierung
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
        Person deserialized = (Person) in.readObject();
        in.close();

        System.out.println("Vor der Serialisierung: " + original);
        System.out.println("Nach der Deserialisierung: " + deserialized);
    }
}

Ergebnis:

Vor der Serialisierung: Person{name='Alice', age=30}
Nach der Deserialisierung: Person{name='Alice', age=-1}

Wie Sie sehen, wurde das Alter nicht serialisiert – alles wie geplant!

5. Besonderheiten und Feinheiten

Wann sollte man writeReplace/readResolve verwenden?

  • Wenn nur ein Teil des Objektzustands serialisiert werden soll.
  • Für die Serialisierung/Deserialisierung von Proxy-Objekten.
  • Zur Unterstützung des Patterns Singleton.
  • Für unveränderliche oder komplexe Objekte, deren innere Struktur sich ändern kann.

Wann sollte man sie nicht verwenden?

  • Wenn man mit transient-Feldern oder writeObject/readObject auskommt.
  • Wenn das Objekt nicht durch ein anderes ersetzt werden soll.

Kompatibilität mit Vererbung

Wenn die Superklasse writeReplace/readResolve definiert, werden sie auch für Unterklassen aufgerufen (sofern sie nicht überschrieben werden). Seien Sie vorsichtig mit Hierarchien!

6. Typische Fehler bei benutzerdefinierter Serialisierung

Fehler Nr. 1: Falsche Sichtbarkeit der Methoden. Wenn writeReplace/readResolve nicht private sind, ruft die Serialisierung sie nicht auf. Nur private!

Fehler Nr. 2: Falscher Rückgabetyp. writeReplace/readResolve müssen Object zurückgeben. Auch wenn Sie tatsächlich Ihren eigenen Typ zurückgeben — deklarieren Sie die Methode mit dem Rückgabetyp Object.

Fehler Nr. 3: Datenverlust. Wenn das Proxy-Objekt nicht alle nötigen Daten zur Wiederherstellung des ursprünglichen Objekts enthält, gehen Informationen verloren. Prüfen Sie stets, dass Sie das Objekt wiederherstellen können.

Fehler Nr. 4: Verletzung von Invarianten. readResolve muss ein Objekt zurückgeben, das den Erwartungen des Programms entspricht (zum Beispiel beim Singleton genau INSTANCE).

Fehler Nr. 5: Unbehandelte Ausnahmen. writeReplace/readResolve können eine ObjectStreamException auslösen. Behandeln Sie sie oder leiten Sie sie explizit weiter.

1
Aufgabe
JAVA 25 SELF, Level 43, Lektion 3
Gesperrt
Kosmischer Nexus: Gewährleistung der Einzigartigkeit des Daseins
Kosmischer Nexus: Gewährleistung der Einzigartigkeit des Daseins
1
Aufgabe
JAVA 25 SELF, Level 43, Lektion 3
Gesperrt
Speicher für geheime Daten: Maskierung von Informationen bei der Übertragung
Speicher für geheime Daten: Maskierung von Informationen bei der Übertragung
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION