CodeGym /Kursy /JAVA 25 SELF /Obiekty zagnieżdżone i hierarchiczne: serializacja grafów...

Obiekty zagnieżdżone i hierarchiczne: serializacja grafów

JAVA 25 SELF
Poziom 44 , Lekcja 1
Dostępny

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.

1
Zadanie
JAVA 25 SELF, poziom 44, lekcja 1
Niedostępne
Mapowanie podziemnych tuneli z cykliczną trasą 🚇
Mapowanie podziemnych tuneli z cykliczną trasą 🚇
1
Zadanie
JAVA 25 SELF, poziom 44, lekcja 1
Niedostępne
Zarządzanie asortymentem hipermarketu: hierarchia i kolekcje produktów 🏪
Zarządzanie asortymentem hipermarketu: hierarchia i kolekcje produktów 🏪
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION