Was ist das und wofür?
Serialisierung – die Umwandlung eines Objekts in einen Byte-Stream (zum Schreiben in eine Datei/Übertragung über das Netzwerk/Caching). Deserialisierung – die umgekehrte Wiederherstellung eines Objekts aus Bytes.
Wo findet man es:
- verteilte Systeme (RPC, REST, Kafka), Sitzungen (HttpSession), Caches (Redis, Hazelcast), Warteschlangen (JMS), Snapshot des Zustands.
Basisserialisierung: Serializable
Minimales Beispiel
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // fixiert die Klassengeneration
private String username;
private transient String password; // nicht serialisiert
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
// Schreiben
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(new User("alice", "p@ss"));
}
// Lesen
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User u = (User) in.readObject();
}Wichtige Punkte
- Der Marker-Interface
Serializable(ohne Methoden) erlaubt den Standardmechanismus. serialVersionUID– die Klassengeneration. Wenn nicht angegeben, generiert die JVM automatisch → bei geringfügigen Änderungen der Struktur könnenInvalidClassExceptionauftreten.
Manuelle Fixierung vonserialVersionUIDstabilisiert die Kompatibilität.transientkennzeichnet Felder, die nicht in den Stream aufgenommen werden sollten (Passwörter, Caches, Abhängigkeiten, lazy-Proxys usw.).
Anpassung des Prozesses
1. writeObject/readObject
Ermöglicht die Kontrolle über Serialisierung/Deserialisierung.
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Standardlogik
// zusätzlich verschlüsseln und schreiben
String enc = password == null ? null : encrypt(password);
out.writeObject(enc);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Standardlogik
String enc = (String) in.readObject();
this.password = enc == null ? null : decrypt(enc);
}2. readResolve / writeReplace
readResolve()– gibt das ersetzenende Objekt nach der Deserialisierung zurück (nützlich für Singleton/Caches).writeReplace()– ersetzt das Objekt vor der Serialisierung (Pattern Serialization Proxy).
// Singleton
public class AppConfig implements Serializable {
private static final AppConfig INSTANCE = new AppConfig();
private AppConfig() {}
public static AppConfig getInstance() { return INSTANCE; }
private Object readResolve() { return INSTANCE; } // behalten der Singleton-Garantie
}3. serialPersistentFields
Definiert das „logische“ Set serialisierbarer Felder, unabhängig von den tatsächlichen.
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("username", String.class)
};Volle Kontrolle: Externalizable
Wenn eine vollständige manuelle Serialisierung benötigt wird:
import java.io.*;
public class Point implements Externalizable {
private int x, y;
public Point() {} // obligatorischer öffentlicher Konstruktor ohne Argumente
public Point(int x, int y) { this.x = x; this.y = y; }
@Override public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(x); out.writeInt(y);
}
@Override public void readExternal(ObjectInput in) throws IOException {
this.x = in.readInt(); this.y = in.readInt();
}
}Vorteile: Kontrolle über Format, Geschwindigkeit, Kompatibilität. Nachteile: mehr Code, höheres Risiko von Fehlern.
Sicherheit: Filterung und Beschränkungen (JEP 290)
Problem: Java-Serialisierung kann zu Sicherheitslücken (Gadget-Chains, RCE) bei der Deserialisierung unvertrauenswürdiger Daten führen.
Lösungen:
- ObjectInputFilter (JDK 9+): weiße/schwarze Listen von Klassen, Grenzwert des Graphen.
import java.io.*;
import java.util.Objects;
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"maxdepth=50;maxbytes=1048576;!*;java.base/*;com.myapp.*"
);
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("in.bin"))) {
in.setObjectInputFilter(filter);
Object obj = in.readObject();
}- Serialisieren Sie niemals Daten aus externen Quellen mit der Standard-Java-Serialisierung.
- Für Netzprotokolle – alternative Formate (siehe unten).
Kompatibilität von Versionen (Versionierung)
Was bricht die Kompatibilität:
- Entfernung/Änderung des Feldtyps;
- Änderung der Hierarchie (Eltern);
- Umbenennung der Klasse/des Pakets;
- Kein geeigneter Konstruktor (für
Externalizable).
Strategien:
serialVersionUIDfixieren;- für neue Felder – Standardwerte in
readObjectgeben; - Serialization Proxy verwenden: serialisiert ein einfaches „DTO-Proxy“, nicht das komplexe Objekt selbst;
- nach Möglichkeit – auf Protokolle mit expliziter Schema (Protobuf/Avro) umsteigen.
Performance und Fallstricke
- Langsamer und lauter für GC als binäre spezialisierte Formate (Protobuf/Kryo).
- Schlecht über Sprachen hinweg tragbar (Java-spezifisch).
- Zyklische Objektgraphen werden unterstützt, können aber die Größe erhöhen.
- JPA-Entitäten mit LAZY-Feldern: Proxy-Objekte von Bibliotheken werden oft nicht serialisiert →
NotSerializableException. Für DTOs – Mapping (MapStruct) statt direkter Serialisierung. transient-Felder nach Deserialisierung werdennull/0/false sein – vergessen Sie nicht, sie korrekt zu initialisieren.
Alternativen (empfohlen in der Produktion)
1. JSON (häufig Jackson)
Vorteile: lesbar, plattformübergreifend, einfach zu debuggen. Nachteile: Volumen, Geschwindigkeit geringer als bei binären Formaten.
// Jackson
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper om = new ObjectMapper();
String json = om.writeValueAsString(new User("alice", "p@ss")); // Passwort kann @JsonIgnore sein
User u = om.readValue(json, User.class);Annotationen: @JsonProperty, @JsonIgnore, @JsonCreator, @JsonTypeInfo (Polymorphismus), @JsonInclude.
2. Protocol Buffers (Google Protobuf)
Vorteile: schnell, kompakt, strikte Schema mit Versionierung, ausgezeichnete plattformübergreifende Unterstützung. Nachteile: binär (nicht lesbar), erfordert Kompilierung .proto.
// user.proto
syntax = "proto3";
package demo;
message User {
string username = 1;
string email = 2;
}Generieren wir Java-Klassen (protoc), dann:
UserProto.User u = UserProto.User.newBuilder()
.setUsername("alice").setEmail("a@x.io").build();
byte[] bytes = u.toByteArray();
UserProto.User restored = UserProto.User.parseFrom(bytes);3. Avro
Vorteile: dynamisches/evolutionäres Schema (oft in Kafka), Kompression, binäres Format, gut für Ereignisprotokollierung. Nachteile: etwas schwierigerer Start als JSON.
4. Kryo
Vorteile: sehr schnell, kompakt, kann beliebige Graphen serialisieren. Nachteile: empfindlich gegenüber Klasseneränderungen, erfordert Kontrolle über Registrierungen, eher in-JVM/Caches/Spark, als öffentliche API.
Best Practices (Zusammenfassung)
- Vermeiden Sie die Standard-Java-Serialisierung in öffentlichen Protokollen und bei der Arbeit mit unvertrauenswürdigen Daten.
serialVersionUIDfixieren.- Als
transientmarkieren oder DTOs ohne Geheimnisse verwenden. - Für feine Logik –
writeObject/readObject,readResolve/writeReplace, Serialization Proxy. - Für Sicherheit – ObjectInputFilter (JEP 290), Grenzen für Tiefe/Bytes/Klassen.
- Für intersprachliche Kommunikation und stabile Versionierung – Protobuf/Avro; für menschenlesbare Daten – JSON/Jackson.
- Bei JPA/LAZY – serialisieren Sie DTOs, nicht Entitäten.
- In verteilten Caches/Warteschlangen – verwenden Sie unterstützte Serialisierer (Kafka Serde, Redis Codecs, Spring MessageConverter).