1. Serialisierung von Collections innerhalb von Collections
In Java können Collections nicht nur einfache Typen enthalten (zum Beispiel String), sondern auch andere Collections oder Objekte. Das eröffnet die Möglichkeit, komplexe Strukturen zu erstellen, etwa Map<String, List<User>>, wobei User – Ihre eigene Klasse – ist.
Beispiel: Serialisierung einer Map mit verschachtelter List
Betrachten wir das Beispiel eines kleinen sozialen Netzwerks, in dem jeder Benutzer eine Freundesliste hat.
import java.io.*;
import java.util.*;
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name;
User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + '}';
}
}
public class SocialNetwork implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<User>> friends = new HashMap<>();
public static void main(String[] args) throws IOException, ClassNotFoundException {
SocialNetwork network = new SocialNetwork();
network.friends.put("alice", Arrays.asList(new User("bob"), new User("carol")));
network.friends.put("bob", Collections.singletonList(new User("alice")));
// Serialisierung
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
out.writeObject(network);
}
// Deserialisierung
SocialNetwork loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
loaded = (SocialNetwork) in.readObject();
}
System.out.println("Wiederhergestelltes Netzwerk: " + loaded.friends);
}
}
Man kann es so sagen: Alle verwendeten Typen – sei es HashMap, ArrayList oder User – implementieren das Interface Serializable. Bei der Serialisierung durchläuft Java automatisch alle verschachtelten Collections und Objekte und schreibt sie ebenfalls. Daher erhalten Sie nach der Deserialisierung die Struktur vollständig wiederhergestellt, einschließlich aller verschachtelten Listen.
Ausgabe:
Wiederhergestelltes Netzwerk: {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}
Verschachtelung nach Belieben
Sie können beliebig viele Verschachtelungsebenen erstellen: List<List<User>>, Map<String, Map<Integer, List<User>>> – Java fürchtet sich nicht vor Rekursion (natürlich im vernünftigen Rahmen).
2. Hierarchische Objekte: Serialisierung von Collections mit Vererbung
Was ist, wenn Ihre Collections Objekte enthalten, die dem Vererbungsprinzip folgen? Zum Beispiel haben Sie eine Basisklasse Animal, und in der Collection liegen sowohl Cat als auch Dog?
Beispiel: Serialisierung einer Collection mit Unterklassen
import java.io.*;
import java.util.*;
abstract class Animal implements Serializable {
private static final long serialVersionUID = 1L;
String name;
Animal(String name) {
this.name = name;
}
public abstract String speak();
}
class Cat extends Animal {
private static final long serialVersionUID = 1L;
Cat(String name) {
super(name);
}
@Override
public String speak() {
return "Meow!";
}
}
class Dog extends Animal {
private static final long serialVersionUID = 1L;
Dog(String name) {
super(name);
}
@Override
public String speak() {
return "Woof!";
}
}
public class Zoo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
List<Animal> animals = new ArrayList<>();
animals.add(new Cat("Fluffy"));
animals.add(new Dog("Buddy"));
// Serialisierung
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
out.writeObject(animals);
}
// Deserialisierung
List<Animal> loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("zoo.ser"))) {
loaded = (List<Animal>) in.readObject();
}
for (Animal animal : loaded) {
System.out.println(animal.name + " sagt: " + animal.speak());
}
}
}
Ausgabe:
Fluffy sagt: Meow!
Buddy sagt: Woof!
Wichtiger Punkt: Java serialisiert nicht nur die Felder der Basisklasse, sondern auch Informationen über den tatsächlichen Objekttyp. Daher behalten die Objekte nach der Deserialisierung ihre „katzenartige“ bzw. „hundartige“ Identität, und Sie können ihre Methoden sicher aufrufen.
3. Serialisierung von Objektgraphen
Jetzt ist es Zeit für die eigentliche Magie – die Serialisierung von Objektgraphen, bei denen sich Objekte gegenseitig referenzieren können und nicht nur ineinander verschachtelt sind. Doch zunächst klären wir, was solche Graphen sind.
Was ist ein Objektgraph?
Ein Objektgraph ist eine Struktur, in der Objekte über Referenzfelder miteinander verbunden sind. Zum Beispiel können in einem Stammbaum Personen Referenzen auf Eltern, Kinder sowie Brüder und Schwestern haben.
Analogie: Stellen Sie sich eine Gruppe von Freunden in einem sozialen Netzwerk vor: Jeder Benutzer hat eine Freundesliste, und diese Freunde sind ebenfalls Benutzer mit eigenen Freunden usw. Das ist ein Objektgraph.
Beispiel: Serialisierung einer doppelt verketteten Liste
import java.io.*;
class Node implements Serializable {
private static final long serialVersionUID = 1L;
String value;
Node next;
Node prev;
Node(String value) {
this.value = value;
}
}
public class DoublyLinkedListDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// Wir erstellen zwei verknüpfte Knoten
Node first = new Node("A");
Node second = new Node("B");
first.next = second;
second.prev = first;
// Serialisierung
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
out.writeObject(first);
}
// Deserialisierung
Node loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
loaded = (Node) in.readObject();
}
System.out.println("Wert des ersten: " + loaded.value); // "A"
System.out.println("Nächster: " + loaded.next.value); // "B"
System.out.println("Vorgänger des Nächsten: " + loaded.next.prev.value); // "A"
}
}
Beachten Sie: Es wird nur eine Referenz serialisiert (first), aber dank rekursiver Serialisierung „läuft“ Java über alle verbundenen Objekte. Bei der Deserialisierung wird die Referenzstruktur vollständig wiederhergestellt: loaded.next.prev == loaded ist true! Und wenn der Graph Zyklen enthält (zum Beispiel wenn sich Knoten gegenseitig referenzieren), funktioniert die Standard-Serialisierung von Java korrekt und gerät nicht in eine Endlosschleife.
4. Verschachtelte und hierarchische Collections: Beispiel mit einer realen Klasse
Modell: Buchkatalog
Angenommen, wir haben eine Klasse Book, die ein gedrucktes Buch oder eine elektronische Ausgabe sein kann (Vererbung). Außerdem gibt es die Klasse Library, die eine Map der Genres enthält (Map<String, List<Book>>). Jedes Genre ist eine Liste von Büchern.
import java.io.*;
import java.util.*;
abstract class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
Book(String title) {
this.title = title;
}
}
class PaperBook extends Book {
private static final long serialVersionUID = 1L;
int pages;
PaperBook(String title, int pages) {
super(title);
this.pages = pages;
}
}
class EBook extends Book {
private static final long serialVersionUID = 1L;
String format;
EBook(String title, String format) {
super(title);
this.format = format;
}
}
class Library implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<Book>> catalog = new HashMap<>();
}
public class CatalogDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Library library = new Library();
library.catalog.put("Science-Fiction", Arrays.asList(
new PaperBook("Dune", 800),
new EBook("Der Marsianer", "epub")
));
library.catalog.put("Klassiker", Collections.singletonList(
new PaperBook("Krieg und Frieden", 1200)
));
// Serialisierung
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
out.writeObject(library);
}
// Deserialisierung
Library loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("library.ser"))) {
loaded = (Library) in.readObject();
}
for (Map.Entry<String, List<Book>> entry : loaded.catalog.entrySet()) {
System.out.println("Genre: " + entry.getKey());
for (Book book : entry.getValue()) {
System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
}
}
}
}
Ausgabe:
Genre: Science-Fiction
- Dune (PaperBook)
- Der Marsianer (EBook)
Genre: Klassiker
- Krieg und Frieden (PaperBook)
Fazit:
- Die Serialisierung verschachtelter Collections (Map<String, List<Book>>) funktioniert „out of the box“.
- Die Objekttypen (PaperBook, EBook) bleiben erhalten.
- Nach der Deserialisierung wird die Struktur vollständig wiederhergestellt.
5. Serialisierung von Objektgraphen: Was passiert „unter der Haube“?
Wenn Sie ein Objekt serialisieren, „läuft“ Java über alle seine Felder (und die Felder der Felder usw.) und serialisiert jedes Objekt nur einmal. Wenn ein Objekt erneut auftaucht (z. B. in einer zyklischen Referenz), schreibt Java eine spezielle Referenz und serialisiert es nicht erneut.
Visualisierung (Flussdiagramm)
graph TD
A[Objekt A] -- Feld --> B[Objekt B]
B -- Feld --> C[Objekt C]
C -- Feld --> A
Java serialisiert zuerst A, dann B, dann C, und wenn A erneut angetroffen wird, schreibt sie eine „Referenz auf das bereits serialisierte Objekt A“. Bei der Deserialisierung wird die Struktur unter Beibehaltung aller Verknüpfungen wiederhergestellt.
6. Besonderheiten der Serialisierung von Graphen
- Zyklen sind kein Problem: Die Standard-Serialisierung von Java unterstützt zyklische Referenzen, gerät nicht in Endlosschleifen und verursacht keinen StackOverflow.
- Alle Objekte müssen serialisierbar sein: Wenn auch nur ein Objekt im Graphen nicht Serializable implementiert, schlägt die Serialisierung bei diesem Objekt fehl.
- Gleiche Objekte werden nicht dupliziert: Wenn dasselbe Objekt an mehreren Stellen des Graphen vorkommt, ist es nach der Deserialisierung dasselbe Objekt (per Referenz).
- Die Objekttypen bleiben erhalten: Selbst wenn die Collection als List<Animal> deklariert ist, erhalten Sie nach der Deserialisierung Objekte ihrer tatsächlichen Klassen (Cat, Dog usw.).
7. Typische Fehler bei der Serialisierung verschachtelter und hierarchischer Objekte
Fehler Nr. 1: Nicht alle Klassen sind serialisierbar.
Sehr oft wird vergessen, implements Serializable zu einer der eigenen Klassen hinzuzufügen, die innerhalb einer Collection oder eines verschachtelten Objekts liegt. Ergebnis: NotSerializableException und Enttäuschung. Prüfen Sie die gesamte Verschachtelungskette!
Fehler Nr. 2: Verlust von Referenzen bei manueller Serialisierung.
Wenn Sie die Methoden writeObject/readObject selbst implementieren und dabei vergessen, eines der Felder zu serialisieren (zum Beispiel eine Referenz auf das Elternobjekt oder auf eine verschachtelte Collection), ist die Struktur nach der Deserialisierung beschädigt. Testen Sie die Wiederherstellung immer.
Fehler Nr. 3: Verwendung von transient für benötigte Felder.
Wenn Sie ein benötigtes Feld als transient markieren, landet es nicht im serialisierten Stream und ist nach der Wiederherstellung null oder hat den Standardwert. Das kann die Integrität des Objektgraphen verletzen.
Fehler Nr. 4: Änderung der Klassenstruktur zwischen Serialisierung und Deserialisierung.
Wenn Sie die Klassenstruktur geändert haben (z. B. ein Feld hinzugefügt), nachdem das Objekt serialisiert wurde, kann es bei der Deserialisierung zu Fehlern oder Datenverlust kommen. Verwenden Sie serialVersionUID und wahren Sie die Kompatibilität.
Fehler Nr. 5: Serialisierung großer Graphen.
Komplexe, stark verknüpfte Strukturen können zu sehr großen Dateien und langen Serialisierungs-/Deserialisierungszeiten führen. Behalten Sie die Größen im Blick und teilen Sie nach Möglichkeit auf.
Fehler Nr. 6: Serialisierung „roher“ Collections.
Wenn Sie eine Collection ohne Generic-Parameter deklarieren (zum Beispiel einfach List), müssen Sie nach der Deserialisierung die Typen explizit casten, was zu ClassCastException führen kann. Verwenden Sie Generics und prüfen Sie die Typen.
GO TO FULL VERSION