1. Serializacja kolekcji wewnątrz kolekcji
W Java kolekcje mogą zawierać nie tylko proste typy (na przykład String), ale także inne kolekcje lub obiekty. Otwiera to drogę do tworzenia złożonych struktur: na przykład Map<String, List<User>>, gdzie User — twoja własna klasa.
Przykład: serializacja Map z zagnieżdżoną List
Rozważmy przykład małej sieci społecznościowej, w której każdy użytkownik ma listę znajomych.
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")));
// Serializacja
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
out.writeObject(network);
}
// Deserializacja
SocialNetwork loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
loaded = (SocialNetwork) in.readObject();
}
System.out.println("Odtworzona sieć: " + loaded.friends);
}
}
Można powiedzieć tak: wszystkie używane typy — czy to HashMap, ArrayList czy User — implementują interfejs Serializable. Podczas serializacji Java automatycznie przechodzi przez wszystkie zagnieżdżone kolekcje i obiekty, zapisując je również. Dlatego po deserializacji otrzymujesz w pełni odtworzoną strukturę, włącznie ze wszystkimi zagnieżdżonymi listami.
Wynik:
Odtworzona sieć: {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}
Zagnieżdżanie do woli
Możesz tworzyć dowolnie wiele poziomów zagnieżdżenia: List<List<User>>, Map<String, Map<Integer, List<User>>> — Java nie boi się rekurencji (oczywiście w rozsądnych granicach).
2. Hierarchiczne obiekty: serializacja kolekcji z dziedziczeniem
Co jeśli twoje kolekcje zawierają obiekty zbudowane według zasady dziedziczenia? Na przykład masz klasę bazową Animal, a w kolekcji znajdują się zarówno Cat, jak i Dog?
Przykład: serializacja kolekcji z potomkami
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("Murka"));
animals.add(new Dog("Rex"));
// Serializacja
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
out.writeObject(animals);
}
// Deserializacja
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 + " mówi: " + animal.speak());
}
}
}
Wynik:
Murka mówi: Meow!
Rex mówi: Woof!
Ważna uwaga: Java serializuje nie tylko pola klasy bazowej, ale także informację o rzeczywistym typie obiektu. Dlatego po deserializacji obiekty zachowują swoją „kocią” lub „psią” tożsamość i możesz bezpiecznie wywoływać ich metody.
3. Serializacja grafów obiektów
Czas przejść do prawdziwej magii — serializacji grafów obiektów, gdzie obiekty mogą odwoływać się do siebie nawzajem, a nie tylko być zagnieżdżone jeden w drugim. Ale najpierw wyjaśnijmy, czym są takie grafy.
Czym jest graf obiektów?
Graf obiektów to struktura, w której obiekty mogą być powiązane ze sobą przez pola referencyjne. Na przykład w drzewie genealogicznym każda osoba może mieć referencje do rodziców, dzieci, braci i sióstr.
Analogia: Wyobraźmy sobie grupę znajomych w serwisie społecznościowym: każdy użytkownik ma listę znajomych, a ci znajomi to też użytkownicy, którzy mają swoich znajomych i tak dalej. To właśnie graf obiektów.
Przykład: serializacja listy dwukierunkowej
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 {
// Tworzymy dwa powiązane węzły
Node first = new Node("A");
Node second = new Node("B");
first.next = second;
second.prev = first;
// Serializacja
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
out.writeObject(first);
}
// Deserializacja
Node loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
loaded = (Node) in.readObject();
}
System.out.println("Wartość pierwszego: " + loaded.value); // "A"
System.out.println("Następny: " + loaded.next.value); // "B"
System.out.println("Poprzedni u następnego: " + loaded.next.prev.value); // "A"
}
}
Zwróć uwagę: serializowana jest tylko jedna referencja (first), ale dzięki rekurencyjnej serializacji Java „przejdzie” po wszystkich powiązanych obiektach. Przy deserializacji struktura referencji zostanie w pełni odtworzona: loaded.next.prev == loaded będzie true! A jeśli w grafie są cykle (na przykład gdy węzły wskazują na siebie nawzajem), standardowa serializacja Java działa poprawnie i nie wpada w nieskończoną pętlę.
4. Zagnieżdżone i hierarchiczne kolekcje: przykład z realną klasą
Model: katalog książek
Załóżmy, że mamy klasę Book, która może być zwykłą książką lub wydaniem elektronicznym (dziedziczenie). Jest też klasa Library, która zawiera mapę gatunków (Map<String, List<Book>>). Każdy gatunek to lista książek.
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("Fantastyka", Arrays.asList(
new PaperBook("Diuna", 800),
new EBook("Marsjanin", "epub")
));
library.catalog.put("Klasyka", Collections.singletonList(
new PaperBook("Wojna i pokój", 1200)
));
// Serializacja
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
out.writeObject(library);
}
// Deserializacja
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("Gatunek: " + entry.getKey());
for (Book book : entry.getValue()) {
System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
}
}
}
}
Wynik:
Gatunek: Fantastyka
- Diuna (PaperBook)
- Marsjanin (EBook)
Gatunek: Klasyka
- Wojna i pokój (PaperBook)
Podsumowanie:
- Serializacja zagnieżdżonych kolekcji (Map<String, List<Book>>) działa „od ręki”.
- Typy obiektów (PaperBook, EBook) są zachowywane.
- Po deserializacji struktura jest w pełni odtwarzana.
5. Serializacja grafów obiektów: co dzieje się „pod maską”?
Gdy serializujesz obiekt, Java „chodzi” po wszystkich jego polach (i polach ich pól itd.), serializując każdy obiekt tylko raz. Jeśli obiekt pojawia się ponownie (na przykład w cyklicznym odwołaniu), Java zapisuje specjalne odwołanie, a nie serializuje go ponownie.
Wizualizacja (schemat blokowy)
graph TD
A[Obiekt A] -- pole --> B[Obiekt B]
B -- pole --> C[Obiekt C]
C -- pole --> A
Java najpierw serializuje A, potem B, następnie C, a gdy ponownie natrafia na A, zapisuje „odwołanie do już zserializowanego obiektu A”. Przy deserializacji struktura zostaje odtworzona z zachowaniem wszystkich powiązań.
6. Specyfika serializacji grafów
- Cykle nie są problemem: standardowa serializacja Java obsługuje cykliczne referencje, nie zapętla się i nie powoduje StackOverflow.
- Wszystkie obiekty muszą być serializowalne: jeśli choć jeden obiekt w grafie nie implementuje Serializable, serializacja zakończy się niepowodzeniem na tym obiekcie.
- Te same obiekty nie są duplikowane: jeśli ten sam obiekt występuje w kilku miejscach grafu, po deserializacji będzie to ten sam obiekt (ta sama referencja).
- Typy obiektów są zachowywane: nawet jeśli kolekcja jest zadeklarowana jako List<Animal>, po deserializacji otrzymasz obiekty ich rzeczywistych klas (Cat, Dog itd.).
7. Typowe błędy przy serializacji zagnieżdżonych i hierarchicznych obiektów
Błąd nr 1: Nie wszystkie klasy są serializowalne.
Bardzo często zapomina się dodać implements Serializable do jednej z własnych klas znajdujących się wewnątrz kolekcji lub obiektu zagnieżdżonego. W efekcie — NotSerializableException i rozczarowanie. Sprawdzaj całą ścieżkę zagnieżdżeń!
Błąd nr 2: Utrata referencji przy ręcznej serializacji.
Jeśli implementujesz metody writeObject/readObject samodzielnie i zapomnisz zserializować jedno z pól (na przykład referencję do rodzica lub do zagnieżdżonej kolekcji), po deserializacji struktura będzie uszkodzona. Zawsze testuj odtwarzanie.
Błąd nr 3: Użycie transient dla potrzebnych pól.
Jeśli oznaczysz potrzebne pole jako transient, nie trafi ono do strumienia serializacji, a po odtworzeniu będzie miało wartość null lub domyślną. Może to naruszyć integralność grafu obiektów.
Błąd nr 4: Zmiana struktury klas między serializacją a deserializacją.
Jeśli zmienisz strukturę klasy (na przykład dodasz pole) po zserializowaniu obiektu, przy próbie deserializacji możliwe są błędy lub utrata danych. Używaj serialVersionUID i utrzymuj kompatybilność.
Błąd nr 5: Serializacja dużych grafów.
Złożone, silnie powiązane struktury mogą prowadzić do bardzo dużych plików i długiej serializacji/deserializacji. Monitoruj rozmiary i w miarę możliwości dziel na części.
Błąd nr 6: Serializacja „surowych” kolekcji.
Jeśli zadeklarujesz kolekcję bez parametru generycznego (na przykład po prostu List), po deserializacji trzeba będzie jawnie rzutować typy, co grozi ClassCastException. Używaj generyków i weryfikuj typy.
GO TO FULL VERSION