CodeGym /Kurse /JAVA 25 SELF /Serialisierung generischer Collections: Besonderheiten

Serialisierung generischer Collections: Besonderheiten

JAVA 25 SELF
Level 45 , Lektion 1
Verfügbar

1. Serialisierung und Deserialisierung generischer Collections

Generics (Obgeneralisierungen) in Java sind keine Magie, sondern eher eine Illusion, die der Compiler aufrechterhält. Beim Kompilieren werden Informationen über die Typparameter von Generics getilgt (das nennt man type erasure, also „Streichung der Typinformationen“). Das heißt: Zur Laufzeit (Runtime) unterscheidet sich eine List<String> nicht von einer List<Object> oder List<Integer>. Sie sind alle einfach nur List, und die JVM weiß nicht, welche konkreten Typen darin liegen.

Schauen wir uns ein Beispiel an:

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(stringList.getClass() == intList.getClass()); // true!

Hier ist es etwas trickreich. Wir erstellen zwei Listen – eine für Strings, die andere für Zahlen. Auf Compiler-Ebene achtet Java streng darauf, dass Sie in eine List<String> nichts anderes als Strings legen und in eine List<Integer> nichts anderes als Zahlen. Sobald das Programm jedoch läuft, verschwinden die Unterschiede. Für die JVM sind beide Objekte einfach ArrayList, und sie kann nicht mehr prüfen, welche Elemente tatsächlich gespeichert werden sollen. Genau deshalb liefert der Klassenvergleich der beiden Listen (stringList.getClass() == intList.getClass()) true.

Daraus folgt eine wichtige Erkenntnis: Generics in Java dienen in erster Linie der Bequemlichkeit und Sicherheit zur Compile-Zeit. Zur Laufzeit gehen diese „Labels“ jedoch verloren. Wenn Sie also eine Collection serialisieren, landen nur die eigentlichen Daten in der Datei, nicht aber Informationen über Generic-Typen. Es wird also die Liste der Werte gespeichert, aber aus der Datei lässt sich nicht erkennen, ob es genau eine List<String> war und nicht etwa eine List<Object> oder List<Integer>.

Noch ein Beispiel: Serialisierung und Deserialisierung von List<String>

import java.io.*;
import java.util.*;

public class GenericSerializationDemo {
    public static void main(String[] args) throws Exception {
        List<String> fruits = new ArrayList<>();
        fruits.add("Apfel");
        fruits.add("Banane");
        fruits.add("Orange");

        // Serialisierung
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
            oos.writeObject(fruits);
        }

        // Deserialisierung
        List<String> loadedFruits;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
            loadedFruits = (List<String>) ois.readObject();
        }

        System.out.println(loadedFruits); // [Apfel, Banane, Orange]
    }
}

Achten Sie auf die Zeile:

loadedFruits = (List<String>) ois.readObject();

Hier casten wir das Ergebnis explizit zu List<String>, obwohl es zur Laufzeit eigentlich nur ein ArrayList ist. Der Compiler kann nicht prüfen, ob es sich tatsächlich um eine Liste von Strings handelt – und falls dort plötzlich keine Strings liegen, bekommen wir eine ClassCastException zur Laufzeit.

2. Probleme bei der Deserialisierung generischer Collections

Verlust der Typinformation der Elemente

Da Informationen über Generic-Parameter getilgt werden, kann Java nach der Deserialisierung nicht garantieren, dass in der Collection genau die Objekte liegen, die Sie erwarten. Alles, was Sie bekommen, ist eine „rohe“ Collection (raw type); der Compiler meckert nicht, aber das Problem kann zur Laufzeit auftreten.

Demonstration des Problems

List rawList = new ArrayList();
rawList.add("Katze");
rawList.add(42); // Integer!
// Deserialisierung
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!

Unchecked cast warning

Der Compiler warnt zu Recht vor einem potenziellen Problem:

Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Diese Warnung besagt, dass Sie ohne Prüfung casten und sich in der Collection Objekte unerwarteter Typen befinden können.

3. Besonderheiten der Serialisierung generischer Collections

Keine Information zu Generic-Parametern in der Datei

Wenn Sie eine List<String> und eine List<Integer> serialisieren, wird in der Datei keinerlei Information darüber enthalten sein, ob es Strings oder Zahlen waren. Der Inhalt der Collection wird „wie er ist“ serialisiert – Objekte der Reihe nach.

Wenn Sie die serialisierte Datei in einem Texteditor öffnen, werden Sie dort kein Wort über <String> oder <Integer> sehen. All das existiert nur auf Ebene des Quellcodes und des Compilers.

Beispiel: Serialisierung unterschiedlicher Collections

List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("eins", "zwei", "drei");

// Serialisierung
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
    oos.writeObject(numbers);
    oos.writeObject(words);
}

In der Datei test.ser befinden sich einfach zwei Objekte des Typs ArrayList, ohne jegliche Information über Generic-Parameter.

Problem der Deserialisierung von „rohen“ Collections

Wenn Sie eine Liste ohne Generic-Parameter (raw type) serialisiert, aber als List<String> deserialisiert haben, kann der Compiler die Typen nicht prüfen, und es sind Laufzeitfehler möglich.

4. Best Practices bei der Serialisierung generischer Collections

Dokumentieren Sie die erwarteten Elementtypen.
Wenn Ihre API eine Collection serialisiert, geben Sie unbedingt an, welcher Elementtyp erwartet wird. Zum Beispiel: „Diese Methode gibt eine serialisierte List<User> zurück.“

Überprüfen Sie die Elementtypen nach der Deserialisierung.
Nach der Deserialisierung einer Collection ist es hilfreich zu prüfen, ob alle Elemente den erwarteten Typ haben (insbesondere, wenn die Datenquelle nicht unter Ihrer Kontrolle steht).

for (Object obj : loadedList) {
    if (!(obj instanceof String)) {
        throw new IllegalStateException("Es wurde String erwartet, gefunden: " + obj.getClass());
    }
}

Verwenden Sie unveränderliche Collections.
Wenn Sie eine nur-lesbare Collection serialisieren, verwenden Sie unveränderliche Collections – List.copyOf, Collections.unmodifiableList. Das hilft, unbeabsichtigte Änderungen der Daten nach der Deserialisierung zu vermeiden.

Mischen Sie keine Typen in einer Collection.
Serialisieren Sie nach Möglichkeit keine Collections mit Elementen unterschiedlicher Typen (z. B. eine List<Object> mit verschiedenen Klassen darin). Das erschwert die Deserialisierung und kann zu Fehlern führen.

Gehen Sie vorsichtig mit dem Unterdrücken von Warnungen um.
Wenn Sie sicher sind, dass Sie eine Collection mit korrektem Elementtyp deserialisieren, können Sie die Compiler-Warnung mit der Annotation @SuppressWarnings("unchecked") unterdrücken:

@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();

Tun Sie dies aber mit Bedacht – Probleme lassen sich so leicht bis in die Produktion verbergen.

5. Beispiel: Serialisierung und Deserialisierung einer Collection mit eigener Klasse

Angenommen, wir haben die Klasse User:

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return name + " (" + age + ")";
    }
}

Wir serialisieren eine Liste von Benutzern:

List<User> users = Arrays.asList(
    new User("Alice", 30),
    new User("Bob", 25)
);

// Serialisierung
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
    oos.writeObject(users);
}

// Deserialisierung
List<User> loadedUsers;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.ser"))) {
    loadedUsers = (List<User>) ois.readObject();
}

System.out.println(loadedUsers); // [Alice (30), Bob (25)]

Alles funktioniert! Aber wenn jemand in die serialisierte Datei ein Objekt einer anderen Klasse unterschiebt, können Sie eine ClassCastException bekommen, wenn Sie die Elemente als User lesen.

6. Serialisierung verschachtelter generischer Collections

Collections können verschachtelt sein, zum Beispiel: List<List<String>>, Map<String, List<User>> usw. Java serialisiert solche Strukturen rekursiv, aber die Regeln bleiben dieselben:

  • Alle verschachtelten Collections und Elemente müssen serialisierbar sein.
  • Information über Generic-Parameter wird weiterhin getilgt.

Beispiel: Serialisierung einer Liste von Listen

List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));

// Serialisierung
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
    oos.writeObject(matrix);
}

// Deserialisierung
List<List<String>> loadedMatrix;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("matrix.ser"))) {
    loadedMatrix = (List<List<String>>) ois.readObject();
}

System.out.println(loadedMatrix); // [[a, b], [c, d]]

7. Nützliche Details

Serialisierung generischer Collections mit unterschiedlichen Implementierungen

Manchmal serialisieren Sie eine Implementierung einer Collection und deserialisieren als eine andere. Zum Beispiel: Sie haben einen ArrayList serialisiert, deserialisieren aber als LinkedList. Das führt zu einem Typumwandlungsfehler:

List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!

Tipp: Deserialisieren Sie immer in denselben Typ, den Sie serialisiert haben, oder verwenden Sie das Interface (List), wenn Ihnen die konkrete Implementierung egal ist.

Einsatz von Bibliotheken (z. B. Gson, Jackson)

JSON-Bibliotheken (z. B. Gson, Jackson) können Collections mit Generics serialisieren/deserialisieren, benötigen aber aufgrund des Type Erasure bei der Deserialisierung eine explizite Typangabe. Beispiel für Gson:

Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);

8. Generics und Serialisierung in Map und Set

Alle obigen Regeln gelten auch für andere Collections mit Generics:

  • Bei der Serialisierung von Map<String, Integer> werden Informationen über die Typen der Schlüssel und Werte nicht gespeichert.
  • Bei der Deserialisierung müssen Sie auf den gewünschten Typ casten und auf den Inhalt achten.

Beispiel: Serialisierung einer Map

Map<String, Integer> scores = new HashMap<>();
scores.put("John", 90);
scores.put("Peter", 85);

// Serialisierung
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
    oos.writeObject(scores);
}

// Deserialisierung
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
    loadedScores = (Map<String, Integer>) ois.readObject();
}

System.out.println(loadedScores); // {John=90, Peter=85}

9. Häufige Fehler bei der Serialisierung generischer Collections

Fehler Nr. 1: ClassCastException bei der Deserialisierung. Wenn Sie eine Collection als List<String> deserialisieren, in der sich aber ein Objekt eines anderen Typs befindet, erhalten Sie zur Laufzeit eine ClassCastException. Prüfen Sie immer den Inhalt der Collection!

Fehler Nr. 2: NotSerializableException wegen eines nicht serialisierbaren Elements. Wenn auch nur ein Element der Collection Serializable nicht implementiert, endet die Serialisierung mit einer NotSerializableException. Prüfen Sie die Serialisierbarkeit aller Klassen, die in der Collection vorkommen können.

Fehler Nr. 3: Verlust von Informationen über Generic-Parameter. Verlassen Sie sich nach der Deserialisierung nicht auf Generic-Parameter – zur Laufzeit sind sie nicht vorhanden. Verwenden Sie explizite Typprüfungen, falls Sie Zweifel an der Korrektheit der Daten haben.

Fehler Nr. 4: Inkonsistente Collection-Implementierungen. Sie haben einen ArrayList serialisiert, deserialisieren aber als LinkedList – das führt zu einem Cast-Fehler. Versuchen Sie, in denselben Typ zu deserialisieren, der serialisiert wurde.

Fehler Nr. 5: Inkompatible Klassenversionen. Wenn sich die Struktur der Klassen eines Collection-Elements nach der Serialisierung geändert hat (z. B. wurde ein Feld hinzugefügt), sind Fehler bei der Deserialisierung möglich. Verwenden Sie serialVersionUID zur Versionskontrolle.

1
Aufgabe
JAVA 25 SELF, Level 45, Lektion 1
Gesperrt
Die geheimnisvolle Box: Überprüfung des Inhalts nach der Entnahme
Die geheimnisvolle Box: Überprüfung des Inhalts nach der Entnahme
1
Aufgabe
JAVA 25 SELF, Level 45, Lektion 1
Gesperrt
Persönlicher Einkaufsassistent: Speicherung des Wochenplans
Persönlicher Einkaufsassistent: Speicherung des Wochenplans
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION