1. NotSerializableException: wenn sich eine Collection nicht serialisieren lässt
Der häufigste und zugleich tückischste Fehler bei der Serialisierung von Collections ist die java.io.NotSerializableException. Sie tritt auf, wenn mindestens ein Element der Collection das Interface Serializable nicht implementiert.
Sehen wir uns ein naives Beispiel an:
import java.io.*;
import java.util.*;
class Book {
String title;
Book(String title) { this.title = title; }
}
public class LibraryApp {
public static void main(String[] args) throws Exception {
List<Book> books = new ArrayList<>();
books.add(new Book("Dombey und Sohn"));
// Versuch, die Sammlung zu serialisieren
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
oos.writeObject(books); // Boom! NotSerializableException
}
}
}
Was passiert? Beim Schritt oos.writeObject(books) erhalten Sie die Ausnahme:
java.io.NotSerializableException: Book
Warum? Weil die Klasse Book das Interface Serializable nicht implementiert. Selbst wenn die Collection (ArrayList) serialisierbar ist, müssen die Elemente der Collection ebenfalls serialisierbar sein!
So diagnostizieren Sie
In der Fehlermeldung ist stets die Klasse genannt, die das Problem verursacht hat – suchen Sie sie im Exception-Text. Ist die Collection groß und tritt der Fehler nur unter bestimmten Bedingungen auf, wurde möglicherweise ein Element versehentlich hinzugefügt, das Serializable nicht implementiert.
So beheben Sie den Fehler
Fügen Sie Ihrer Klasse implements Serializable hinzu:
class Book implements Serializable {
String title;
Book(String title) { this.title = title; }
}
Tipp: Wenn die Collection unterschiedliche Objekttypen enthält, prüfen Sie sie alle auf Serializable!
2. ClassCastException bei der Deserialisierung: wenn Generics tückisch werden
In Java wird die Information über Generic-Parameter von Collections nach der Kompilierung getilgt (Type Erasure). Das bedeutet: Wenn Sie eine List<String> serialisiert und als List<Integer> deserialisieren, merkt der Compiler den Fehler nicht, zur Laufzeit erhalten Sie jedoch eine ClassCastException.
Beispiel:
// Serialisierung
List<String> names = Arrays.asList("Anna", "Boris");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("names.ser"))) {
oos.writeObject(names);
}
// Deserialisierung (GEFÄHRLICH!)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("names.ser"))) {
List<Integer> numbers = (List<Integer>) ois.readObject(); // unchecked cast
Integer first = numbers.get(0); // Boom! ClassCastException
}
Fehler:
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
So vermeiden Sie das
- Verwenden Sie keine „rohen“ Collections (raw types) und casten Sie nicht ohne Not.
- Prüfen Sie die Elementtypen nach der Deserialisierung, wenn Sie sich über den Inhalt nicht sicher sind.
- Dokumentieren Sie, welcher Collection-Typ serialisiert wird und beim Lesen erwartet wird.
Beispiel für eine sichere Deserialisierung:
Object obj = ois.readObject();
if (obj instanceof List<?>) {
List<?> list = (List<?>) obj;
if (!list.isEmpty() && list.get(0) instanceof String) {
@SuppressWarnings("unchecked")
List<String> safeNames = (List<String>) obj; // Warnung unterdrückt, aber der Typ wurde geprüft!
}
}
3. Änderung der Klassenstruktur: serialVersionUID und Abwärtskompatibilität
Sie haben eine Collection serialisiert und dann beschlossen, der Elementklasse ein neues Feld hinzuzufügen, einen Feldnamen zu ändern oder die Klassenstruktur insgesamt zu verändern. Beim Versuch, die alte Datei zu deserialisieren, erhalten Sie nun eine rätselhafte Fehlermeldung:
java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 5678
Warum passiert das?
Jede serialisierbare Klasse erhält eine eindeutige Versionskennung – serialVersionUID. Wenn sich die Klasse ändert (zum Beispiel fügen Sie ein Feld hinzu), berechnet die JVM eine neue serialVersionUID, und bei der Deserialisierung wird festgestellt, dass die Version der Klasse nicht mit der beim Serialisieren übereinstimmt.
So vermeiden Sie das
- Deklarieren Sie serialVersionUID ausdrücklich in Ihren Klassen:
class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
// ...
}
- Bewahren Sie Abwärtskompatibilität: Entfernen oder benennen Sie Felder nicht um, wenn Sie alte Dateien lesen möchten.
- Testen Sie die Deserialisierung nach Änderungen.
Was tun, wenn die Klasse dennoch geändert werden muss?
- Erwägen Sie die Implementierung der Methoden readObject/writeObject für eine manuelle Steuerung der Serialisierung.
- Oder migrieren Sie die Daten: Lesen Sie die alte Datei mit der alten Klassenversion ein und speichern Sie sie anschließend im neuen Format neu.
4. Datenverlust bei der Serialisierung unveränderlicher Collections
In neueren Java-Versionen gibt es unveränderliche Collections, zum Beispiel erstellt über List.of(), Set.of(), Map.of(). In älteren Java-Versionen (vor 12) und in einigen Drittanbieter-Implementierungen kann die Serialisierung solcher Collections fehlerhaft sein: Nach der Deserialisierung wird die Collection veränderlich oder es tritt ein Fehler auf.
Beispiel:
List<String> list = List.of("a", "b", "c");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
oos.writeObject(list);
}
Auf älteren JVMs trat bei der Deserialisierung ein Fehler auf oder die Collection war anschließend nicht mehr unveränderlich.
So vermeiden Sie das
- Prüfen Sie die Dokumentation für die verwendete Java-Version.
- Testen Sie Serialisierung und Deserialisierung solcher Collections.
- Wenn Sie die Unveränderlichkeit bewahren müssen, umhüllen Sie die Collection nach der Deserialisierung mit Collections.unmodifiableList(list).
5. Serialisierung von transient- und static-Feldern
Was passiert mit solchen Feldern:
- transient – mit diesem Schlüsselwort markierte Felder werden überhaupt nicht serialisiert. Nach der Deserialisierung haben sie den Standardwert (zum Beispiel null oder 0).
- static – Klassenfelder (und nicht Objektfelder) werden niemals serialisiert.
Beispiel:
class Book implements Serializable {
String title;
transient String cache; // wird nicht serialisiert!
static String publisher = "Default"; // wird ebenfalls nicht serialisiert!
}
Warum ist das wichtig
Wenn Sie berechnete Werte oder einen Cache im Objekt speichern, markieren Sie diese als transient – das spart Platz und beschleunigt die Serialisierung.
Achtung: Nach der Deserialisierung müssen transient-Felder neu berechnet oder erneut initialisiert werden.
6. Serialisierung großer Collections: Leistung und Dateigröße
Probleme:
- Große Collections (zum Beispiel eine Million Objekte) können zu sehr großen Dateien, langen Schreib-/Lesezeiten und gelegentlich sogar zu Speichermangel führen (OutOfMemoryError).
- Bei der Serialisierung eines Objektgraphen (zum Beispiel komplexer, miteinander verbundener Collections) kann die Dateigröße unerwartet stark anwachsen.
So vermeiden Sie das
- Serialisieren Sie die Collection in Teilen: Schreiben Sie Objekte beispielsweise einzeln oder in kleinen Batches.
- Verwenden Sie Streaming-Verarbeitung: Statt die gesamte Collection auf einmal zu serialisieren, serialisieren Sie Elemente nach Bedarf.
- Komprimieren Sie Dateien: Nutzen Sie GZIPOutputStream, um die Dateigröße zu reduzieren.
Beispiel für eine Streaming-Serialisierung:
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
for (Book book : bigList) {
oos.writeObject(book);
}
}
Achtung: Bei diesem Ansatz muss die Deserialisierung wissen, wie viele Objekte geschrieben wurden (oder einen speziellen „Endmarker“ verwenden).
GO TO FULL VERSION