Updated for Java 25 — includes Record Serialization & Deserialization Filters.

Klassische Serialization

// Klasse serialisierbar machen
public class User implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    private transient String password; // wird NICHT serialisiert
}

Serialisieren & Deserialisieren

// Schreiben
try (var oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
    oos.writeObject(user);
}

// Lesen
try (var ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
    User user = (User) ois.readObject();
}

Serialization anpassen

public class User implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    // Eigene Serialisierungslogik
    @Serial
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeUTF(encrypt(password));   // transient-Felder manuell schreiben
    }

    @Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.password = decrypt(in.readUTF());
    }

    // Schutz vor gefälschten Streams
    @Serial
    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("Stream data required");
    }

    // Ersatzobjekt bei Serialisierung zurückgeben
    @Serial
    private Object writeReplace() throws ObjectStreamException {
        return new UserProxy(this);
    }

    // Ersatzobjekt bei Deserialisierung zurückgeben
    @Serial
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE; // z.B. für Singletons
    }
}

@Serial Annotation (Java 14+)

Markiert Methoden und Felder, die zur Serialisierung gehören. Der Compiler warnt, wenn die Signatur nicht stimmt.

@Serial private static final long serialVersionUID = 1L;
@Serial private static final ObjectStreamField[] serialPersistentFields = { ... };
@Serial private void writeObject(ObjectOutputStream out) { ... }
@Serial private void readObject(ObjectInputStream in) { ... }
@Serial private void readObjectNoData() { ... }
@Serial private Object writeReplace() { ... }
@Serial private Object readResolve() { ... }

Record Serialization (Java 16+)

Records haben eine eigene, sichere Serialisierung — der kanonische Konstruktor wird immer verwendet.

// Einfach Serializable implementieren — fertig!
public record User(String name, int age) implements Serializable {}

// Serialisierung nutzt automatisch:
//   - Component Accessors zum Schreiben (name(), age())
//   - Kanonischen Konstruktor zum Lesen (new User(name, age))
//
// IGNORIERT bei Records:
//   - writeObject, readObject, readObjectNoData
//   - writeExternal, readExternal
//   - serialPersistentFields

Warum Records bevorzugen?

// Klassisch: Deserialisierung umgeht den Konstruktor → Sicherheitsrisiko!
public class User implements Serializable {
    private final String name;
    public User(String name) {
        Objects.requireNonNull(name);  // wird bei Deserialisierung NICHT aufgerufen!
        this.name = name;
    }
}

// Record: Konstruktor wird IMMER aufgerufen → Validierung greift
public record User(String name) implements Serializable {
    public User {
        Objects.requireNonNull(name);  // wird auch bei Deserialisierung aufgerufen!
    }
}

Externalizable

Volle Kontrolle über das Format — aber mehr Aufwand.

public class User implements Externalizable {
    private String name;
    private int age;

    public User() {}   // public No-Arg-Konstruktor PFLICHT!

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        this.name = in.readUTF();
        this.age = in.readInt();
    }
}

Deserialization Filters (Java 9+ / Java 17+)

Schutz vor Deserialization-Angriffen.

Stream-spezifischer Filter

var ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filterInfo -> {
    if (filterInfo.serialClass() != null) {
        // Nur bestimmte Klassen erlauben
        if (filterInfo.serialClass() == User.class) {
            return ObjectInputFilter.Status.ALLOWED;
        }
        return ObjectInputFilter.Status.REJECTED;
    }
    // Limits prüfen
    if (filterInfo.depth() > 5) return ObjectInputFilter.Status.REJECTED;
    if (filterInfo.references() > 100) return ObjectInputFilter.Status.REJECTED;
    return ObjectInputFilter.Status.UNDECIDED;
});

Pattern-basierter Filter

// Erlaubt nur bestimmte Klassen, begrenzt Tiefe und Referenzen
var filter = ObjectInputFilter.Config.createFilter(
    "com.myapp.model.*;!*;maxdepth=5;maxrefs=100"
);
ois.setObjectInputFilter(filter);

// Syntax:
//   com.myapp.*   → Paket erlauben
//   !*            → alles andere ablehnen
//   maxdepth=N    → max. Verschachtelungstiefe
//   maxrefs=N     → max. Anzahl Referenzen
//   maxbytes=N    → max. Bytes
//   maxarray=N    → max. Array-Länge

JVM-weite Filter-Factory (Java 17+, JEP 415)

// In main() oder per System Property setzen
ObjectInputFilter.Config.setSerialFilterFactory((current, next) -> {
    // Kombiniere globalen Filter mit stream-spezifischem
    var baseFilter = ObjectInputFilter.Config.createFilter(
        "com.myapp.**;!*"
    );
    return ObjectInputFilter.merge(next, baseFilter);
});

// Oder per JVM Property:
// -Djdk.serialFilter=com.myapp.**;!*
// -Djdk.serialFilterFactory=com.myapp.MyFilterFactory

serialVersionUID

// Explizit setzen → Kompatibilität kontrollieren
@Serial
private static final long serialVersionUID = 1L;

// Wenn nicht gesetzt: JVM berechnet automatisch basierend auf Klassenstruktur
// → Jede Änderung an der Klasse bricht Deserialisierung!

// Tipp: Immer explizit setzen und bei inkompatiblen Änderungen hochzählen

Kompatible vs. inkompatible Änderungen

Kompatibel Inkompatibel
Felder hinzufügen Felder entfernen/umbenennen
Zugriffsmodifier ändern Feldtyp ändern
transient hinzufügen/entfernen Klassenhierarchie ändern
Methoden ändern Serializable entfernen
  static zu/von Feld ändern

Best Practices

1. Records bevorzugen — sichere Deserialisierung über Konstruktor
2. @Serial verwenden — Compiler fängt Signatur-Fehler ab
3. serialVersionUID immer setzen — explizite Versionskontrolle
4. Deserialization Filter nutzen — Schutz vor Angriffen
5. transient für sensible Daten — Passwörter, Tokens, etc.
6. readResolve für Singletons — verhindert Duplikate
7. Alternativen prüfen — JSON (Jackson/Gson) oft besser geeignet
8. Defensive Deserialisierung — Eingaben im readObject validieren