CodeGym /Kursy /JAVA 25 SELF /Serializacja zagnieżdżonych obiektów: specyfika

Serializacja zagnieżdżonych obiektów: specyfika

JAVA 25 SELF
Poziom 44, Lekcja 0
Dostępny

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ę.

1
Zadanie
JAVA 25 SELF, poziom 44, lekcja 0
Niedostępne
Cyfrowa kartoteka akt osobowych 🏢
Cyfrowa kartoteka akt osobowych 🏢
1
Zadanie
JAVA 25 SELF, poziom 44, lekcja 0
Niedostępne
Zarządzanie skomplikowaną logistyką dostaw: głęboko zagnieżdżone dane 🌍
Zarządzanie skomplikowaną logistyką dostaw: głęboko zagnieżdżone dane 🌍
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION