1. Wprowadzenie: serializacja obiektów z polami‑referencjami
W rzeczywistych aplikacjach rzadko spotyka się całkowicie „płaskie” klasy. Zazwyczaj obiekt zawiera inne obiekty, a te z kolei mogą zawierać jeszcze coś dalej. Nazywa się to kompozycją (lub zagnieżdżeniem) obiektów. Na przykład:
public class Address {
String city;
String street;
}
public class Person {
String name;
int age;
Address address; // Zagnieżdżony obiekt!
}
Gdy serializujemy taki obiekt, pojawia się pytanie: co zrobić z polem address? Czy Java powinna serializować je razem z Person? A jeśli Address zawiera w środku kolejny obiekt? Na szczęście (albo niestety – zależy od sytuacji) Java domyślnie serializuje wszystkie zagnieżdżone obiekty rekursywnie, o ile one również implementują interfejs Serializable.
Serializacja w Javie to zawsze serializacja głęboka (deep serialization). Oznacza to, że serializowany jest nie tylko sam obiekt, ale także wszystkie obiekty, do których się odwołuje przez swoje (nie transient) pola, i tak dalej – aż do samego „dna”.
Wizualizacja procesu
graph TD
A[Person] --> B[Address]
B --> C[CityInfo]
A --> D[Pet]
Krótko mówiąc, jeśli postanowisz zserializować Person, Java zserializuje także Address i wszystko, co znajduje się wewnątrz Address, i tak dalej.
2. Wymagania dla zagnieżdżonych obiektów: Serializable jest obowiązkowe!
Zapewne zwróciłeś już uwagę na ważny niuans: wszystkie zagnieżdżone obiekty, które są serializowane, również muszą implementować interfejs Serializable.
Jeśli choć jedno pole‑referencja wskazuje na obiekt, który nie implementuje Serializable, próba serializacji zakończy się wyjątkiem java.io.NotSerializableException.
Przyjrzyjmy się przykładom serializacji głębokiej.
Przykład: wszystko w porządku
import java.io.Serializable;
public class Address implements Serializable {
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Obie klasy implementują Serializable. Wszystko działa, serializacja przebiega pomyślnie.
Przykład: błąd!
public class Address { // NIE implementuje Serializable!
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Jak widać, tutaj Address nie implementuje interfejsu Serializable. Zatem przy serializacji Person otrzymamy wyjątek NotSerializableException.
3. Przykład: serializacja i deserializacja z obiektem zagnieżdżonym
Zobaczmy, jak wygląda to w praktyce. Na poprzednim poziomie pracowaliśmy z aplikacją „Menadżer kontaktów”. Dodajmy teraz do niej adres każdego użytkownika.
import java.io.*;
class Address implements Serializable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Person implements Serializable {
String name;
int age;
Address address;
Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
public class SerializationDemo {
public static void main(String[] args) throws Exception {
Person p = new Person("Ivan", 30, new Address("Praga", "Slavinska, 1"));
// Serializacja
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Deserializacja
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name + ", " + restored.age + ", " +
restored.address.city + ", " + restored.address.street);
}
}
Wynik:
Ivan, 30, Praga, Slavinska, 1
Wszystko działa: zagnieżdżony obiekt Address został zserializowany i odtworzony razem z Person.
4. Co jeśli zagnieżdżony obiekt nie jest serializowalny?
Jeśli spróbujesz zserializować obiekt, którego choć jedno pole‑referencja wskazuje na obiekt nieimplementujący Serializable, Java rzuci wyjątek już przy próbie serializacji.
Demonstracja błędu
class Address { // nie implementuje Serializable!
String city;
String street;
}
class Person implements Serializable {
String name;
Address address;
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.name = "Peter";
p.address = new Address();
p.address.city = "Derri";
p.address.street = "Elm";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p); // <-- BĘDZIE WYJĄTEK!
out.close();
}
}
Błąd:
java.io.NotSerializableException: Address
5. Transient dla zagnieżdżonych obiektów
Co zrobić, jeśli masz pole‑referencję do obiektu, który nie powinien być serializowany (np. cache, połączenie z bazą danych, obiekt tymczasowy)? W takim przypadku oznacz pole jako transient. Wtedy Java po prostu pominie to pole podczas serializacji.
Przykład z transient
class Address { // nie implementuje Serializable
String city;
String street;
}
class Person implements Serializable {
String name;
transient Address address; // transient!
Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Address addr = new Address();
addr.city = "Los-Santos";
addr.street = "Mulholland Drive";
Person p = new Person("Alexander", addr);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Deserializacja
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // "Alexander"
System.out.println(restored.address); // null!
}
}
Wynik:
Pole address po deserializacji ma wartość null, ponieważ jest transient.
6. Głębokie zagnieżdżenie i rekurencja
Serializacja działa rekurencyjnie: jeśli Person ma pole Address, a Address ma pole CityInfo i tak dalej, serializator będzie „zanurzał się” coraz głębiej, aż napotka coś nieserializowalnego albo… skończy się pamięć (żart, ale tylko w połowie).
Ważne: cykliczne referencje
Serializator Javy potrafi obsługiwać cykliczne referencje. Jeśli na przykład obiekt ma referencję do innego obiektu, który z kolei odwołuje się z powrotem, serializator nie wpadnie w pętlę, lecz poprawnie zapisze strukturę.
class A implements Serializable {
B b;
}
class B implements Serializable {
A a;
}
Jeśli utworzysz obiekty A i B, które wskazują na siebie nawzajem, serializacja nie spowoduje StackOverflowError – Java zapamiętuje już zserializowane obiekty.
7. Przykład: serializacja obiektu z listą zagnieżdżoną
Często wewnątrz obiektu znajdują się kolekcje innych obiektów. Na przykład użytkownik może mieć listę znajomych:
import java.io.*;
import java.util.*;
class Person implements Serializable {
String name;
List<Person> friends;
Person(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
}
public class FriendsSerialization {
public static void main(String[] args) throws Exception {
Person alice = new Person("Alice");
Person bob = new Person("Bob");
alice.friends.add(bob);
// Serializacja
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
out.writeObject(alice);
out.close();
// Deserializacja
ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // Alice
System.out.println(restored.friends.get(0).name); // Bob
}
}
Ważne: Kolekcje standardowej biblioteki Javy (ArrayList, HashMap itd.) implementują Serializable, więc wszystko działa od razu.
8. Typowe błędy przy serializacji zagnieżdżonych obiektów
Błąd nr 1: Jeden z zagnieżdżonych obiektów nie implementuje Serializable. Zapomniałeś dodać implements Serializable w jednej z klas zagnieżdżonych. Efekt – NotSerializableException przy pierwszej próbie serializacji. Sprawdź, czy wszystkie klasy w Twoim łańcuchu serializacji wspierają ten interfejs.
Błąd nr 2: Nieserializowalne pole nie zostało oznaczone jako transient. Jeśli masz pole, którego nie należy serializować (np. strumień, połączenie z bazą, coś tymczasowego), ale nie oznaczyłeś go jako transient, serializacja zakończy się błędem. Pamiętaj o transient!
Błąd nr 3: Niedopasowanie serialVersionUID w klasach zagnieżdżonych. Jeśli jawnie deklarujesz serialVersionUID w klasach zagnieżdżonych i zmieniasz ich strukturę, nie zapomnij aktualizować tego identyfikatora – w przeciwnym razie możliwe są błędy podczas deserializacji.
Błąd nr 4: Mutujące (zmienialne) obiekty zagnieżdżone. Jeśli serializujesz kolekcję lub obiekt, który potem się zmienia (np. listę znajomych), po deserializacji otrzymasz „migawkę” z momentu serializacji. Nowe zmiany w oryginale nie pojawią się w deserializowanym obiekcie.
Błąd nr 5: Serializacja ogromnych grafów obiektów. Jeśli masz bardzo złożoną strukturę z wieloma zagnieżdżonymi obiektami, serializacja może zająć dużo czasu i pamięci. Czasem lepiej serializować tylko kluczowe dane, a nie całą strukturę.
GO TO FULL VERSION