CodeGym /Kurse /JAVA 25 SELF /Kontrolle über den Serialisierungsprozess: writeObject, r...

Kontrolle über den Serialisierungsprozess: writeObject, readObject

JAVA 25 SELF
Level 43 , Lektion 0
Verfügbar

1. Einleitung

Automatische Serialisierung ist wie der Autopilot im Flugzeug: Sie funktioniert hervorragend, solange alles nach Plan läuft. Sobald jedoch besondere Bedingungen auftreten, wird klar, dass der einfache Mechanismus nicht mehr ausreicht. Stellen Sie sich vor, Sie müssen ein Objekt speichern, aber nicht alle seine Felder: Manche Daten sind temporär, andere zu sensibel, um sie in eine Datei zu schreiben. Oder umgekehrt – beim Speichern soll etwas Eigenes hinzugefügt werden, z. B. eine Version oder eine Prüfsumme. Es gibt auch Fälle, in denen vor dem Schreiben oder Laden eine Prüfung oder Transformation nötig ist. Und manchmal ist die Aufgabe noch komplexer: Kompatibilität mit früheren Klassenversionen sicherstellen, wenn sich die Struktur im Laufe der Zeit geändert hat.

In solchen Situationen wird klar: Mit der Standardserialisierung allein kommt man nicht aus. Man muss die Kontrolle selbst übernehmen.

Spezielle Serialisierungsmethoden: writeObject und readObject

In Java gibt es zwei spezielle Methoden, mit denen sich der Serialisierungs- und Deserialisierungsprozess eines Objekts vollständig steuern lässt:

private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

Wichtig!

  • Die Methoden müssen private sein (nicht public, nicht protected, nicht package-private).
  • Die Signaturen müssen exakt den oben gezeigten entsprechen.
  • Wenn diese Methoden in Ihrer Klasse deklariert sind, werden sie anstelle der Standardserialisierung/-deserialisierung aufgerufen.

Wie funktioniert das?

Wenn Sie ObjectOutputStream.writeObject(obj) aufrufen, sucht die JVM zuerst in der Klasse von obj nach der Methode private void writeObject(ObjectOutputStream). Wenn sie vorhanden ist, wird genau diese aufgerufen. Analog wird bei der Deserialisierung private void readObject(ObjectInputStream) aufgerufen.

Wenn die Methoden nicht deklariert sind, wird die Standardserialisierung verwendet.

Wie writeObject und readObject aufgebaut sind

Methodensignaturen

private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

Innerhalb dieser Methoden müssen Sie Folgendes aufrufen:

  • out.defaultWriteObject(); – für die Serialisierung der Standardfelder (nicht transient) der Superklasse und der aktuellen Klasse.
  • in.defaultReadObject(); – für die Deserialisierung der Standardfelder.

Wenn Sie diese Methoden nicht aufrufen, werden die Standardfelder nicht serialisiert – und nach der Deserialisierung ist das Objekt „leer“. Das ist, als hätten Sie den Pass nicht in den Koffer gelegt: Formal sind Sie angekommen, aber Sie können nicht nachweisen, wer Sie sind.

2. Beispiel: Prüfsumme bei der Serialisierung hinzufügen

Betrachten wir ein praktisches Beispiel. Angenommen, wir haben eine Benutzerklasse und möchten beim Serialisieren dem Objekt eine Prüfsumme hinzufügen, um bei der Deserialisierung die Datenintegrität zu prüfen.

import java.io.*;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    // transient-Feld – wird nicht serialisiert
    private transient int checksum;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
        this.checksum = calculateChecksum();
    }

    private int calculateChecksum() {
        return (name != null ? name.hashCode() : 0) + age;
    }

    // Benutzerdefinierte Serialisierung
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // Standardfelder speichern
        int sum = calculateChecksum();
        out.writeInt(sum); // Prüfsumme schreiben
        System.out.println("[LOG] Serialisierung User: checksum=" + sum);
    }

    // Benutzerdefinierte Deserialisierung
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // Standardfelder wiederherstellen
        int sum = in.readInt(); // Prüfsumme lesen
        int actual = calculateChecksum();
        System.out.println("[LOG] Deserialisierung User: checksum=" + sum + ", actual=" + actual);
        if (sum != actual) {
            throw new IOException("Daten sind beschädigt! Die Prüfsumme stimmt nicht überein.");
        }
        this.checksum = actual;
    }

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

Beispiel für die Verwendung:

// Objekt speichern
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
    out.writeObject(user);
}

// Objekt laden
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
    User loaded = (User) in.readObject();
    System.out.println("Wiederhergestelltes Objekt: " + loaded);
}

Was passiert hier?

  • Bei der Serialisierung wird writeObject aufgerufen, Standardfelder plus Prüfsumme werden gespeichert.
  • Bei der Deserialisierung wird readObject aufgerufen, die Felder werden wiederhergestellt und die Prüfsumme geprüft.
  • In der Konsole erscheint ein Log, und wenn etwas nicht stimmt – wird eine Ausnahme ausgelöst.

3. Sensible Daten von der Serialisierung ausschließen

Manchmal sollen bestimmte Felder nicht serialisiert werden (z. B. Passwörter). Dafür kann man das Schlüsselwort transient verwenden (mehr dazu – in der nächsten Vorlesung), oder man lässt das Feld manuell weg, wenn man writeObject implementiert.

Beispiel:

public class Account implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password; // transient – wird nicht serialisiert

    // Man kann es auch so machen:
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        // password nicht schreiben!
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        // password bleibt null
    }
}

Achtung:
Wenn Sie nur einen Teil des Objekts serialisieren möchten – schreiben Sie einfach die überflüssigen Felder nicht in den Stream.

4. Aufruf der Superklassenmethoden: defaultWriteObject und defaultReadObject

Innerhalb Ihrer Methoden writeObject und readObject sollten Sie fast immer defaultWriteObject() und defaultReadObject() aufrufen. Das ist, als würde man „Entwurf speichern“ drücken, bevor man eigene Notizen hinzufügt.

Diese Methoden sind für die Standardserialisierung aller nicht‑transienten, nicht‑statichen Felder der aktuellen Klasse und der Superklasse verantwortlich. Wenn man sie nicht aufruft, werden diese Felder nicht serialisiert und sind nach der Deserialisierung leer.

Beispiel für falsches Verhalten:

private void writeObject(ObjectOutputStream out) throws IOException {
    // out.defaultWriteObject(); // Aufruf vergessen!
    out.writeInt(123); // etwas Eigenes
}

In diesem Fall werden die Standardfelder schlicht nicht gespeichert!

5. Praxis: Protokollierung des Serialisierungsprozesses

Fügen wir unserer Benutzerklasse Logging hinzu, damit sichtbar ist, wann Serialisierung und Deserialisierung stattfinden.

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("[LOG] Serialisierung Person: " + name + ", Alter " + age);
        out.defaultWriteObject();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        System.out.println("[LOG] Deserialisierung Person: " + name + ", Alter " + age);
    }
}

Verwendung:

Person p = new Person("Bob", 30);
// In Datei speichern
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
    out.writeObject(p);
}
// Aus Datei laden
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
    Person loaded = (Person) in.readObject();
}

Ergebnis:
In der Konsole sehen Sie Meldungen darüber, dass das Objekt serialisiert und deserialisiert wird.

6. Typische Fehler bei der Verwendung von writeObject/readObject

Fehler Nr. 1: defaultWriteObject/defaultReadObject wurde nicht aufgerufen. Wenn man vergisst, diese Methoden aufzurufen, werden die Standardfelder nicht serialisiert, und das Objekt ist nach der Deserialisierung leer oder inkorrekt.

Fehler Nr. 2: Falsche Methodensignatur. Die Methoden müssen strikt private void writeObject(ObjectOutputStream) und private void readObject(ObjectInputStream) sein. Wenn man sie public/protected macht oder die Parameter ändert – werden sie nicht automatisch aufgerufen.

Fehler Nr. 3: Ausnahme in der Methode. Wenn in writeObject oder readObject eine Ausnahme auftritt, wird die Serialisierung bzw. Deserialisierung abgebrochen und das Objekt nicht korrekt gespeichert/geladen.

Fehler Nr. 4: Vergessene Serialisierung/Deserialisierung der Superklasse. Wenn Ihre Klasse von einer serialisierbaren Superklasse erbt, rufen Sie unbedingt defaultWriteObject/defaultReadObject auf, sonst werden die Felder der Superklasse nicht gespeichert.

Fehler Nr. 5: Serialisierung sensibler Daten. Wenn Sie Passwörter oder andere vertrauliche Daten nicht ausschließen, gelangen sie in die serialisierte Datei. Verwenden Sie transient oder serialisieren Sie sie manuell nicht.

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